Repository: uchuhimo/konf Branch: master Commit: 8aa88358b89f Files: 213 Total size: 832.2 KB Directory structure: gitextract_30e7_42m/ ├── .gitattributes ├── .github/ │ ├── stale.yml │ └── workflows/ │ └── gradle.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc/ │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ ├── Dependencies.kt │ └── Utils.kt ├── config/ │ └── spotless/ │ ├── apache-license-2.0.java │ └── apache-license-2.0.kt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── konf-all/ │ ├── build.gradle.kts │ └── src/ │ ├── snippet/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ └── snippet/ │ │ │ ├── ServerInJava.java │ │ │ └── ServerSpecInJava.java │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ └── snippet/ │ │ │ ├── Config.kt │ │ │ ├── Export.kt │ │ │ ├── Fork.kt │ │ │ ├── Load.kt │ │ │ ├── QuickStart.kt │ │ │ ├── Serialize.kt │ │ │ └── Server.kt │ │ └── resources/ │ │ └── server.json │ └── test/ │ └── kotlin/ │ └── com/ │ └── uchuhimo/ │ └── konf/ │ └── source/ │ ├── MergeSourcesWithDifferentFeaturesSpec.kt │ ├── MultiLayerConfigToValueSpec.kt │ ├── MultipleDefaultLoadersSpec.kt │ └── QuickStartSpec.kt ├── konf-core/ │ ├── build.gradle.kts │ └── src/ │ ├── jmh/ │ │ └── kotlin/ │ │ └── com/ │ │ └── uchuhimo/ │ │ └── konf/ │ │ └── ConfigBenchmark.kt │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ └── Configs.java │ │ └── kotlin/ │ │ └── com/ │ │ └── uchuhimo/ │ │ └── konf/ │ │ ├── BaseConfig.kt │ │ ├── Config.kt │ │ ├── ConfigException.kt │ │ ├── ConfigSpec.kt │ │ ├── Feature.kt │ │ ├── Item.kt │ │ ├── ItemContainer.kt │ │ ├── MergedConfig.kt │ │ ├── MergedMap.kt │ │ ├── Prefix.kt │ │ ├── SizeInBytes.kt │ │ ├── Spec.kt │ │ ├── TreeNode.kt │ │ ├── Utils.kt │ │ ├── annotation/ │ │ │ └── Annotations.kt │ │ └── source/ │ │ ├── DefaultLoaders.kt │ │ ├── DefaultProviders.kt │ │ ├── Loader.kt │ │ ├── MergedSource.kt │ │ ├── Provider.kt │ │ ├── Source.kt │ │ ├── SourceException.kt │ │ ├── SourceNode.kt │ │ ├── Utils.kt │ │ ├── Writer.kt │ │ ├── base/ │ │ │ ├── FlatSource.kt │ │ │ ├── KVSource.kt │ │ │ ├── MapSource.kt │ │ │ └── ValueSource.kt │ │ ├── deserializer/ │ │ │ ├── DurationDeserializer.kt │ │ │ ├── EmptyStringToCollectionDeserializerModifier.kt │ │ │ ├── JSR310Deserializer.kt │ │ │ ├── OffsetDateTimeDeserializer.kt │ │ │ ├── StringDeserializer.kt │ │ │ └── ZoneDateTimeDeserializer.kt │ │ ├── env/ │ │ │ └── EnvProvider.kt │ │ ├── json/ │ │ │ ├── JsonProvider.kt │ │ │ ├── JsonSource.kt │ │ │ └── JsonWriter.kt │ │ └── properties/ │ │ ├── PropertiesProvider.kt │ │ └── PropertiesWriter.kt │ ├── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ ├── AnonymousConfigSpec.java │ │ │ ├── ConfigJavaApiTest.java │ │ │ └── NetworkBufferInJava.java │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ ├── AdHocConfigItemSpec.kt │ │ │ ├── AdHocNetworkBuffer.kt │ │ │ ├── ConfigInJavaSpec.kt │ │ │ ├── ConfigSpecTestSpec.kt │ │ │ ├── ConfigTestSpec.kt │ │ │ ├── FeatureSpec.kt │ │ │ ├── MergedConfigSpek.kt │ │ │ ├── MergedMapSpec.kt │ │ │ ├── MultiLayerConfigSpec.kt │ │ │ ├── NetworkBuffer.kt │ │ │ ├── ParseDurationSpec.kt │ │ │ ├── RelocatedConfigSpec.kt │ │ │ ├── SizeInBytesSpec.kt │ │ │ ├── TreeNodeSpec.kt │ │ │ └── source/ │ │ │ ├── CustomDeserializerSpec.kt │ │ │ ├── DefaultLoadersSpec.kt │ │ │ ├── DefaultProvidersSpec.kt │ │ │ ├── FacadeSourceSpec.kt │ │ │ ├── FallbackSourceSpec.kt │ │ │ ├── LoaderSpec.kt │ │ │ ├── MergedSourceLoadSpec.kt │ │ │ ├── ProviderSpec.kt │ │ │ ├── SourceInfoSpec.kt │ │ │ ├── SourceLoadSpec.kt │ │ │ ├── SourceNodeSpec.kt │ │ │ ├── SourceSpec.kt │ │ │ ├── WriterSpec.kt │ │ │ ├── base/ │ │ │ │ ├── FlatSourceLoadSpec.kt │ │ │ │ ├── FlatSourceSpec.kt │ │ │ │ ├── KVSourceSpec.kt │ │ │ │ ├── MapSourceLoadSpec.kt │ │ │ │ ├── MapSourceSpec.kt │ │ │ │ └── ValueSourceSpec.kt │ │ │ ├── deserializer/ │ │ │ │ ├── DurationDeserializerSpec.kt │ │ │ │ ├── OffsetDateTimeDeserializerSpec.kt │ │ │ │ ├── StringDeserializerSpec.kt │ │ │ │ └── ZonedDateTimeDeserializerSpec.kt │ │ │ ├── env/ │ │ │ │ ├── EnvProviderSpec.kt │ │ │ │ └── env.properties │ │ │ ├── json/ │ │ │ │ ├── JsonProviderSpec.kt │ │ │ │ ├── JsonSourceLoadSpec.kt │ │ │ │ ├── JsonSourceSpec.kt │ │ │ │ └── JsonWriterSpec.kt │ │ │ ├── properties/ │ │ │ │ ├── PropertiesProviderSpec.kt │ │ │ │ └── PropertiesSourceLoadSpec.kt │ │ │ └── serializer/ │ │ │ └── PrimitiveStdSerializerSpec.kt │ │ └── resources/ │ │ └── source/ │ │ ├── provider.properties │ │ ├── source.json │ │ └── source.properties │ └── testFixtures/ │ └── kotlin/ │ └── com/ │ └── uchuhimo/ │ └── konf/ │ ├── TestUtils.kt │ └── source/ │ ├── ConfigForLoad.kt │ ├── SingleThreadDispatcher.kt │ ├── SourceLoadBaseSpec.kt │ ├── TestUtils.kt │ └── base/ │ ├── FlatConfigForLoad.kt │ └── FlatSourceLoadBaseSpec.kt ├── konf-git/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── uchuhimo/ │ │ └── konf/ │ │ └── source/ │ │ ├── DefaultGitLoader.kt │ │ ├── DefaultGitProvider.kt │ │ ├── GitLoader.kt │ │ └── GitProvider.kt │ └── test/ │ └── kotlin/ │ └── com/ │ └── uchuhimo/ │ └── konf/ │ └── source/ │ ├── DefaultGitLoaderSpec.kt │ ├── DefaultGitProviderSpec.kt │ ├── GitLoaderSpec.kt │ └── GitProviderSpec.kt ├── konf-hocon/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── uchuhimo/ │ │ └── konf/ │ │ └── source/ │ │ ├── DefaultHoconLoader.kt │ │ ├── DefaultHoconProvider.kt │ │ └── hocon/ │ │ ├── HoconProvider.kt │ │ ├── HoconSource.kt │ │ └── HoconWriter.kt │ ├── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ ├── LoaderJavaApiTest.java │ │ │ └── NetworkBufferInJava.java │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ └── source/ │ │ │ ├── DefaultHoconLoaderSpec.kt │ │ │ ├── DefaultHoconProviderSpec.kt │ │ │ └── hocon/ │ │ │ ├── HoconProviderSpec.kt │ │ │ ├── HoconSourceLoadSpec.kt │ │ │ ├── HoconSourceSpec.kt │ │ │ ├── HoconValueSourceSpec.kt │ │ │ └── HoconWriterSpec.kt │ │ └── resources/ │ │ └── source/ │ │ └── source.conf │ └── testFixtures/ │ └── kotlin/ │ └── com/ │ └── uchuhimo/ │ └── konf/ │ └── source/ │ └── HoconTestUtils.kt ├── konf-js/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── uchuhimo/ │ │ └── konf/ │ │ └── source/ │ │ ├── DefaultJsLoader.kt │ │ ├── DefaultJsProvider.kt │ │ └── js/ │ │ ├── JsProvider.kt │ │ └── JsWriter.kt │ └── test/ │ ├── kotlin/ │ │ └── com/ │ │ └── uchuhimo/ │ │ └── konf/ │ │ └── source/ │ │ ├── DefaultJsLoaderSpec.kt │ │ ├── DefaultJsProviderSpec.kt │ │ └── js/ │ │ ├── JsProviderSpec.kt │ │ ├── JsSourceLoadSpec.kt │ │ └── JsWriterSpec.kt │ └── resources/ │ └── source/ │ └── source.js ├── konf-toml/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ ├── moandjiezana/ │ │ │ └── toml/ │ │ │ └── Toml4jWriter.kt │ │ └── uchuhimo/ │ │ └── konf/ │ │ └── source/ │ │ ├── DefaultTomlLoader.kt │ │ ├── DefaultTomlProvider.kt │ │ └── toml/ │ │ ├── TomlProvider.kt │ │ └── TomlWriter.kt │ ├── test/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ └── source/ │ │ │ ├── DefaultTomlLoaderSpec.kt │ │ │ ├── DefaultTomlProviderSpec.kt │ │ │ └── toml/ │ │ │ ├── TomlProviderSpec.kt │ │ │ ├── TomlSourceLoadSpec.kt │ │ │ ├── TomlValueSourceSpec.kt │ │ │ └── TomlWriterSpec.kt │ │ └── resources/ │ │ └── source/ │ │ └── source.toml │ └── testFixtures/ │ └── kotlin/ │ └── com/ │ └── uchuhimo/ │ └── konf/ │ └── source/ │ └── TomlTestUtils.kt ├── konf-xml/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── uchuhimo/ │ │ └── konf/ │ │ └── source/ │ │ ├── DefaultXmlLoader.kt │ │ ├── DefaultXmlProvider.kt │ │ └── xml/ │ │ ├── XmlProvider.kt │ │ └── XmlWriter.kt │ ├── test/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ └── source/ │ │ │ ├── DefaultXmlLoaderSpec.kt │ │ │ ├── DefaultXmlProviderSpec.kt │ │ │ └── xml/ │ │ │ ├── XmlProviderSpec.kt │ │ │ ├── XmlSourceLoadSpec.kt │ │ │ └── XmlWriterSpec.kt │ │ └── resources/ │ │ └── source/ │ │ └── source.xml │ └── testFixtures/ │ └── kotlin/ │ └── com/ │ └── uchuhimo/ │ └── konf/ │ └── source/ │ └── XmlTestUtils.kt ├── konf-yaml/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── uchuhimo/ │ │ └── konf/ │ │ └── source/ │ │ ├── DefaultYamlLoader.kt │ │ ├── DefaultYamlProvider.kt │ │ └── yaml/ │ │ ├── YamlProvider.kt │ │ └── YamlWriter.kt │ ├── test/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── uchuhimo/ │ │ │ └── konf/ │ │ │ └── source/ │ │ │ ├── DefaultYamlLoaderSpec.kt │ │ │ ├── DefaultYamlProviderSpec.kt │ │ │ └── yaml/ │ │ │ ├── YamlProviderSpec.kt │ │ │ ├── YamlSourceLoadSpec.kt │ │ │ └── YamlWriterSpec.kt │ │ └── resources/ │ │ └── source/ │ │ └── source.yaml │ └── testFixtures/ │ └── kotlin/ │ └── com/ │ └── uchuhimo/ │ └── konf/ │ └── source/ │ └── YamlTestUtils.kt └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # text stuff * text=lf *.bat text eol=crlf *.cmd text eol=crlf *.java text eol=lf *.kt text eol=lf *.md text eol=lf *.properties text eol=lf *.scala text eol=lf *.sh text eol=lf .gitattributes text eol=lf .gitignore text eol=lf build.gradle text eol=lf gradlew text eol=lf gradlew.bat text eol=crlf gradle/wrapper/gradle-wrapper.properties text eol=lf #binary *.jar binary ================================================ FILE: .github/stale.yml ================================================ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 90 # Number of days of inactivity before a stale Issue or Pull Request is closed daysUntilClose: false # Issues or Pull Requests with these labels will never be considered stale exemptLabels: - bug - Announcement - help wanted - To investigate # Label to use when marking as stale staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 30 days if no further activity occurs, but feel free to re-open a closed issue if needed. ================================================ FILE: .github/workflows/gradle.yml ================================================ name: Konf CI on: [push] jobs: build: name: Build on JDK ${{ matrix.java_version }} and ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: java_version: [8, 11, 16] os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v2 - name: Set up JDK ${{ matrix.java_version }} uses: actions/setup-java@v1 with: java-version: ${{ matrix.java_version }} - name: Build with Gradle run: ./gradlew build ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Java template *.class # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.ear # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* ### Eclipse template *.pydevproject .metadata .gradle bin/ tmp/ *.tmp *.bak *.swp *~.nib local.properties .settings/ .loadpath # Eclipse Core .project # External tool builders .externalToolBuilders/ # Locally stored "Eclipse launch configurations" *.launch # CDT-specific .cproject # JDT-specific (Eclipse Java Development Tools) .classpath # Java annotation processor (APT) .factorypath # PDT-specific .buildpath # sbteclipse plugin .target # TeXlipse plugin .texlipse ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio *.iml ## Directory-based project format: .idea/ # if you remove the above rule, at least ignore the following: # User-specific stuff: # .idea/workspace.xml # .idea/tasks.xml # .idea/dictionaries # Sensitive or high-churn files: # .idea/dataSources.ids # .idea/dataSources.xml # .idea/sqlDataSources.xml # .idea/dynamic.xml # .idea/uiDesigner.xml # Gradle: # .idea/gradle.xml # .idea/libraries # Mongo Explorer plugin: # .idea/mongoSettings.xml ## File-based project format: *.ipr *.iws ## Plugin-specific files: # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties ### Windows template # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk ### Vim template [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist *~ ### SublimeText template # cache files for sublime text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache # workspace files are user-specific *.sublime-workspace # project files should be checked into the repository, unless a significant # proportion of contributors will probably not be using SublimeText # *.sublime-project # sftp configuration file sftp-config.json ### OSX template .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Gradle template .gradle build/ build-eclipse/ */gradle* # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle/wrapper/gradle-wrapper.jar private.properties .vscode .java-version act *.log ================================================ FILE: .travis.yml ================================================ language: java jdk: - oraclejdk11 - openjdk8 - openjdk11 - openjdk15 after_success: - bash <(curl -s https://codecov.io/bash) before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ cache: directories: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Konf [![Java 8+](https://img.shields.io/badge/Java-8+-4c7e9f.svg)](http://java.oracle.com) [![Maven metadata URL](https://img.shields.io/maven-central/v/com.uchuhimo/konf)](https://search.maven.org/artifact/com.uchuhimo/konf) [![JitPack](https://img.shields.io/jitpack/v/github/uchuhimo/konf)](https://jitpack.io/#uchuhimo/konf) [![Build Status](https://travis-ci.org/uchuhimo/konf.svg?branch=master)](https://travis-ci.org/uchuhimo/konf) [![codecov](https://codecov.io/gh/uchuhimo/konf/branch/master/graph/badge.svg)](https://codecov.io/gh/uchuhimo/konf) [![codebeat badge](https://codebeat.co/badges/f69a1574-9d4c-4da5-be73-56fa7b180d2d)](https://codebeat.co/projects/github-com-uchuhimo-konf-master) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) A type-safe cascading configuration library for Kotlin/Java/Android, supporting most configuration formats. ## Features - **Type-safe**. Get/set value in config with type-safe APIs. - **Thread-safe**. All APIs for config is thread-safe. - **Batteries included**. Support sources from JSON, XML, YAML, [HOCON](https://github.com/typesafehub/config/blob/master/HOCON.md), [TOML](https://github.com/toml-lang/toml), properties, map, command line and system environment out of box. - **Cascading**. Config can fork from another config by adding a new layer on it. Each layer of config can be updated independently. This feature is powerful enough to support complicated situation such as configs with different values share common fallback config, which is automatically updated when configuration file changes. - **Self-documenting**. Document config item with type, default value and description when declaring. - **Extensible**. Easy to customize new sources for config or expose items in config. ## Contents - [Konf](#konf) - [Features](#features) - [Contents](#contents) - [Prerequisites](#prerequisites) - [Use in your projects](#use-in-your-projects) - [Maven](#maven) - [Gradle](#gradle) - [Gradle Kotlin DSL](#gradle-kotlin-dsl) - [Maven (master snapshot)](#maven-master-snapshot) - [Gradle (master snapshot)](#gradle-master-snapshot) - [Gradle Kotlin DSL (master snapshot)](#gradle-kotlin-dsl-master-snapshot) - [Quick start](#quick-start) - [Define items](#define-items) - [Use config](#use-config) - [Create config](#create-config) - [Add config spec](#add-config-spec) - [Retrieve value from config](#retrieve-value-from-config) - [Cast config to value](#cast-config-to-value) - [Check whether an item exists in config or not](#check-whether-an-item-exists-in-config-or-not) - [Modify value in config](#modify-value-in-config) - [Subscribe the update event](#subscribe-the-update-event) - [Export value in config as property](#export-value-in-config-as-property) - [Fork from another config](#fork-from-another-config) - [Load values from source](#load-values-from-source) - [Subscribe the update event for load operation](#subscribe-the-update-event-for-load-operation) - [Strict parsing when loading](#strict-parsing-when-loading) - [Path substitution](#path-substitution) - [Prefix/Merge operations for source/config/config spec](#prefixmerge-operations-for-sourceconfigconfig-spec) - [Export/Reload values in config](#exportreload-values-in-config) - [Supported item types](#supported-item-types) - [Optional features](#optional-features) - [Build from source](#build-from-source) - [Breaking Changes](#breaking-changes) - [v0.19.0](#v0190) - [v0.17.0](#v0170) - [v0.15](#v015) - [v0.10](#v010) - [License](#license) ## Prerequisites - JDK 8 or higher - tested on Android SDK 23 or higher ## Use in your projects This library has been published to [Maven Central](https://search.maven.org/artifact/com.uchuhimo/konf) and [JitPack](https://jitpack.io/#uchuhimo/konf). Konf is modular, you can use different modules for different sources: - `konf-core`: for built-in sources (JSON, properties, map, command line and system environment) - `konf-hocon`: for built-in + [HOCON](https://github.com/typesafehub/config/blob/master/HOCON.md) sources - `konf-toml`: for built-in + [TOML](https://github.com/toml-lang/toml) sources - `konf-xml`: for built-in + XML sources - `konf-yaml`: for built-in + YAML sources - `konf-git`: for built-in + Git sources - `konf`: for all sources mentioned above - `konf-js`: for built-in + JavaScript (use GraalVM JavaScript) sources ### Maven ```xml com.uchuhimo konf 1.1.2 ``` ### Gradle ```groovy compile 'com.uchuhimo:konf:1.1.2' ``` ### Gradle Kotlin DSL ```kotlin compile(group = "com.uchuhimo", name = "konf", version = "1.1.2") ``` ### Maven (master snapshot) Add JitPack repository to `` section: ```xml jitpack.io https://jitpack.io ``` Add dependencies: ```xml com.github.uchuhimo konf master-SNAPSHOT ``` ### Gradle (master snapshot) Add JitPack repository: ```groovy repositories { maven { url 'https://jitpack.io' } } ``` Add dependencies: ```groovy compile 'com.github.uchuhimo.konf:konf:master-SNAPSHOT' ``` ### Gradle Kotlin DSL (master snapshot) Add JitPack repository: ```kotlin repositories { maven(url = "https://jitpack.io") } ``` Add dependencies: ```kotlin compile(group = "com.github.uchuhimo.konf", name = "konf", version = "master-SNAPSHOT") ``` ## Quick start 1. Define items in config spec: ```kotlin object ServerSpec : ConfigSpec() { val host by optional("0.0.0.0") val tcpPort by required() } ``` 2. Construct config with items in config spec and values from multiple sources: ```kotlin val config = Config { addSpec(ServerSpec) } .from.yaml.file("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() ``` or: ```kotlin val config = Config { addSpec(ServerSpec) }.withSource( Source.from.yaml.file("server.yml") + Source.from.json.resource("server.json") + Source.from.env() + Source.from.systemProperties() ) ``` This config contains all items defined in `ServerSpec`, and load values from 4 different sources. Values in resource file `server.json` will override those in file `server.yml`, values from system environment will override those in `server.json`, and values from system properties will override those from system environment. If you want to watch file `server.yml` and reload values when file content is changed, you can use `watchFile` instead of `file`: ```kotlin val config = Config { addSpec(ServerSpec) } .from.yaml.watchFile("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() ``` 3. Define values in source. You can define in any of these sources: - in `server.yml`: ```yaml server: host: 0.0.0.0 tcp_port: 8080 ``` - in `server.json`: ```json { "server": { "host": "0.0.0.0", "tcp_port": 8080 } } ``` - in system environment: ```bash SERVER_HOST=0.0.0.0 SERVER_TCPPORT=8080 ``` - in command line for system properties: ```bash -Dserver.host=0.0.0.0 -Dserver.tcp_port=8080 ``` 4. Retrieve values from config with type-safe APIs: ```kotlin data class Server(val host: String, val tcpPort: Int) { fun start() {} } val server = Server(config[ServerSpec.host], config[ServerSpec.tcpPort]) server.start() ``` 5. Retrieve values from multiple sources without using config spec: ```kotlin val server = Config() .from.yaml.file("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() .at("server") .toValue() server.start() ``` ## Define items Config items is declared in config spec, added to config by `Config#addSpec`. All items in same config spec have same prefix. Define a config spec with prefix `local.server`: ```kotlin object ServerSpec : ConfigSpec("server") { } ``` If the config spec is binding with single class, you can declare config spec as companion object of the class: ```kotlin class Server { companion object : ConfigSpec("server") { val host by optional("0.0.0.0") val tcpPort by required() } } ``` The config spec prefix can be automatically inferred from the class name, leading to further simplification like: ```kotlin object ServerSpec : ConfigSpec() { } ``` or ```kotlin class Server { companion object : ConfigSpec() { } } ``` Here are some examples showing the inference convention: `Uppercase` to `uppercase`, `lowercase` to `lowercase`, `SuffixSpec` to `suffix`, `TCPService` to `tcpService`. The config spec can also be nested. For example, the path of `Service.Backend.Login.user` in the following example will be inferred as "service.backend.login.user": ```kotlin object Service : ConfigSpec() { object Backend : ConfigSpec() { object Login : ConfigSpec() { val user by optional("admin") } } } ``` There are three kinds of item: - Required item. Required item doesn't have default value, thus must be set with value before retrieved in config. Define a required item with description: ```kotlin val tcpPort by required(description = "port of server") ``` Or omit the description: ```kotlin val tcpPort by required() ``` - Optional item. Optional item has default value, thus can be safely retrieved before setting. Define an optional item: ```kotlin val host by optional("0.0.0.0", description = "host IP of server") ``` Description can be omitted. - Lazy item. Lazy item also has default value, however, the default value is not a constant, it is evaluated from thunk every time when retrieved. Define a lazy item: ```kotlin val nextPort by lazy { config -> config[tcpPort] + 1 } ``` You can also define config spec in Java, with a more verbose API (compared to Kotlin version in "quick start"): ```java public class ServerSpec { public static final ConfigSpec spec = new ConfigSpec("server"); public static final OptionalItem host = new OptionalItem(spec, "host", "0.0.0.0") {}; public static final RequiredItem tcpPort = new RequiredItem(spec, "tcpPort") {}; } ``` Notice that the `{}` part in item declaration is necessary to avoid type erasure of item's type information. ## Use config ### Create config Create an empty new config: ```kotlin val config = Config() ``` Or an new config with some initial actions: ```kotlin val config = Config { addSpec(Server) } ``` ### Add config spec Add multiple config specs into config: ```kotlin config.addSpec(Server) config.addSpec(Client) ``` ### Retrieve value from config Retrieve associated value with item (type-safe API): ```kotlin val host = config[Server.host] ``` Retrieve associated value with item name (unsafe API): ```kotlin val host = config.get("server.host") ``` or: ```kotlin val host = config("server.host") ``` ### Cast config to value Cast config to a value with the target type: ```kotlin val server = config.toValue() ``` ### Check whether an item exists in config or not Check whether an item exists in config or not: ```kotlin config.contains(Server.host) // or Server.host in config ``` Check whether an item name exists in config or not: ```kotlin config.contains("server.host") // or "server.host" in config ``` Check whether all values of required items exist in config or not: ```kotlin config.containsRequired() ``` Throw exception if some required items in config don't have values: ```kotlin config.validateRequired() ``` ### Modify value in config Associate item with value (type-safe API): ```kotlin config[Server.tcpPort] = 80 ``` Find item with specified name, and associate it with value (unsafe API): ```kotlin config["server.tcpPort"] = 80 ``` Discard associated value of item: ```kotlin config.unset(Server.tcpPort) ``` Discard associated value of item with specified name: ```kotlin config.unset("server.tcpPort") ``` Associate item with lazy thunk (type-safe API): ```kotlin config.lazySet(Server.tcpPort) { it[basePort] + 1 } ``` Find item with specified name, and associate it with lazy thunk (unsafe API): ```kotlin config.lazySet("server.tcpPort") { it[basePort] + 1 } ``` ### Subscribe the update event Subscribe the update event of an item: ```kotlin val handler = Server.host.onSet { value -> println("the host has changed to $value") } ``` Subscribe the update event before every set operation: ```kotlin val handler = Server.host.beforeSet { config, value -> println("the host will change to $value") } ``` or ```kotlin val handler = config.beforeSet { item, value -> println("${item.name} will change to $value") } ``` Subscribe the update event after every set operation: ```kotlin val handler = Server.host.afterSet { config, value -> println("the host has changed to $value") } ``` or ```kotlin val handler = config.afterSet { item, value -> println("${item.name} has changed to $value") } ``` Cancel the subscription: ```kotlin handler.cancel() ``` ### Export value in config as property Export a read-write property from value in config: ```kotlin var port by config.property(Server.tcpPort) port = 9090 check(port == 9090) ``` Export a read-only property from value in config: ```kotlin val port by config.property(Server.tcpPort) check(port == 9090) ``` ### Fork from another config ```kotlin val config = Config { addSpec(Server) } config[Server.tcpPort] = 1000 // fork from parent config val childConfig = config.withLayer("child") // child config inherit values from parent config check(childConfig[Server.tcpPort] == 1000) // modifications in parent config affect values in child config config[Server.tcpPort] = 2000 check(config[Server.tcpPort] == 2000) check(childConfig[Server.tcpPort] == 2000) // modifications in child config don't affect values in parent config childConfig[Server.tcpPort] = 3000 check(config[Server.tcpPort] == 2000) check(childConfig[Server.tcpPort] == 3000) ``` ## Load values from source Use `from` to load values from source doesn't affect values in config, it will return a new child config by loading all values into new layer in child config: ```kotlin val config = Config { addSpec(Server) } // values in source is loaded into new layer in child config val childConfig = config.from.env() check(childConfig.parent === config) ``` All out-of-box supported sources are declared in [`DefaultLoaders`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/main/kotlin/com/uchuhimo/konf/source/DefaultLoaders.kt), shown below (the corresponding config spec for these samples is [`ConfigForLoad`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/test/kotlin/com/uchuhimo/konf/source/ConfigForLoad.kt)): | Type | Usage | Provider | Sample | | ------------------------------------------------------------ | -------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | [HOCON](https://github.com/typesafehub/config/blob/master/HOCON.md) | `config.from.hocon` | [`HoconProvider`](https://github.com/uchuhimo/konf/blob/master/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconProvider.kt) | [`source.conf`](https://github.com/uchuhimo/konf/blob/master/konf-hocon/src/test/resources/source/source.conf) | | JSON | `config.from.json` | [`JsonProvider`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/main/kotlin/com/uchuhimo/konf/source/json/JsonProvider.kt) | [`source.json`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/test/resources/source/source.json) | | properties | `config.from.properties` | [`PropertiesProvider`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/main/kotlin/com/uchuhimo/konf/source/properties/PropertiesProvider.kt) | [`source.properties`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/test/resources/source/source.properties) | | [TOML](https://github.com/toml-lang/toml) | `config.from.toml` | [`TomlProvider`](https://github.com/uchuhimo/konf/blob/master/konf-toml/src/main/kotlin/com/uchuhimo/konf/source/toml/TomlProvider.kt) | [`source.toml`](https://github.com/uchuhimo/konf/blob/master/konf-toml/src/test/resources/source/source.toml) | | XML | `config.from.xml` | [`XmlProvider`](https://github.com/uchuhimo/konf/blob/master/konf-xml/src/main/kotlin/com/uchuhimo/konf/source/xml/XmlProvider.kt) | [`source.xml`](https://github.com/uchuhimo/konf/blob/master/konf-xml/src/test/resources/source/source.xml) | | YAML | `config.from.yaml` | [`YamlProvider`](https://github.com/uchuhimo/konf/blob/master/konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlProvider.kt) | [`source.yaml`](https://github.com/uchuhimo/konf/blob/master/konf-yaml/src/test/resources/source/source.yaml) | | JavaScript | `config.from.js` | [`JsProvider`](https://github.com/uchuhimo/konf/blob/master/konf-js/src/main/kotlin/com/uchuhimo/konf/source/js/JsProvider.kt) | [`source.js`](https://github.com/uchuhimo/konf/blob/master/konf-js/src/test/resources/source/source.js) | | hierarchical map | `config.from.map.hierarchical` | - | [`MapSourceLoadSpec`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/test/kotlin/com/uchuhimo/konf/source/base/MapSourceLoadSpec.kt) | | map in key-value format | `config.from.map.kv` | - | [`KVSourceSpec`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/test/kotlin/com/uchuhimo/konf/source/base/KVSourceSpec.kt) | | map in flat format | `config.from.map.flat` | - | [`FlatSourceLoadSpec`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/test/kotlin/com/uchuhimo/konf/source/base/FlatSourceLoadSpec.kt) | | system environment | `config.from.env()` | [`EnvProvider`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/main/kotlin/com/uchuhimo/konf/source/env/EnvProvider.kt) | - | | system properties | `config.from.systemProperties()` | [`PropertiesProvider`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/main/kotlin/com/uchuhimo/konf/source/properties/PropertiesProvider.kt) | - | These sources can also be manually created using their provider, and then loaded into config by `config.withSource(source)`. All `from` APIs also have their standalone version that return sources without loading them into the config, shown below: | Type | Usage | | ------------------------------------------------------------ | -------------------------------- | | [HOCON](https://github.com/typesafehub/config/blob/master/HOCON.md) | `Source.from.hocon` | | JSON | `Source.from.json` | | properties | `Source.from.properties` | | [TOML](https://github.com/toml-lang/toml) | `Source.from.toml` | | XML | `Source.from.xml` | | YAML | `Source.from.yaml` | | JavaScript | `Source.from.js` | | hierarchical map | `Source.from.map.hierarchical` | | map in key-value format | `Source.from.map.kv` | | map in flat format | `Source.from.map.flat` | | system environment | `Source.from.env()` | | system properties | `Source.from.systemProperties()` | Format of system properties source is same with that of properties source. System environment source follows the same mapping convention with properties source, but with the following name convention: - All letters in name are in uppercase - `.` in name is replaced with `_` HOCON/JSON/properties/TOML/XML/YAML/JavaScript source can be loaded from a variety of input format. Use properties source as example: - From file: `config.from.properties.file("/path/to/file")` - From watched file: `config.from.properties.watchFile("/path/to/file", 100, TimeUnit.MILLISECONDS)` - You can re-trigger the setup process every time the updated file is loaded by `watchFile("/path/to/file") { config, source -> setup(config) }` - From string: `config.from.properties.string("server.port = 8080")` - From URL: `config.from.properties.url("http://localhost:8080/source.properties")` - From watched URL: `config.from.properties.watchUrl("http://localhost:8080/source.properties", 1, TimeUnit.MINUTES)` - You can re-trigger the setup process every time the URL is loaded by `watchUrl("http://localhost:8080/source.properties") { config, source -> setup(config) }` - From Git repository: `config.from.properties.git("https://github.com/uchuhimo/konf.git", "/path/to/source.properties", branch = "dev")` - From watched Git repository: `config.from.properties.watchGit("https://github.com/uchuhimo/konf.git", "/path/to/source.properties", period = 1, unit = TimeUnit.MINUTES)` - You can re-trigger the setup process every time the Git file is loaded by `watchGit("https://github.com/uchuhimo/konf.git", "/path/to/source.properties") { config, source -> setup(config) }` - From resource: `config.from.properties.resource("source.properties")` - From reader: `config.from.properties.reader(reader)` - From input stream: `config.from.properties.inputStream(inputStream)` - From byte array: `config.from.properties.bytes(bytes)` - From portion of byte array: `config.from.properties.bytes(bytes, 1, 12)` If source is from file, file extension can be auto detected. Thus, you can use `config.from.file("/path/to/source.json")` instead of `config.from.json.file("/path/to/source.json")`, or use `config.from.watchFile("/path/to/source.json")` instead of `config.from.json.watchFile("/path/to/source.json")`. Source from URL also support auto-detected extension (use `config.from.url` or `config.from.watchUrl`). The following file extensions can be supported: | Type | Extension | | ---------- | ---------- | | HOCON | conf | | JSON | json | | Properties | properties | | TOML | toml | | XML | xml | | YAML | yml, yaml | | JavaScript | js | You can also implement [`Source`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt) to customize your new source, which can be loaded into config by `config.withSource(source)`. ### Subscribe the update event for load operation Subscribe the update event before every load operation: ```kotlin val handler = config.beforeLoad { source -> println("$source will be loaded") } ``` You can re-trigger the setup process by subscribing the update event after every load operation: ```kotlin val handler = config.afterLoad { source -> setup(config) } ``` Cancel the subscription: ```kotlin handler.cancel() ``` ### Strict parsing when loading By default, Konf extracts desired paths from sources and ignores other unknown paths in sources. If you want Konf to throws exception when unknown paths are found, you can enable `FAIL_ON_UNKNOWN_PATH` feature: ```kotlin config.enable(Feature.FAIL_ON_UNKNOWN_PATH) .from.properties.file("server.properties") .from.json.resource("server.json") ``` Then `config` will validate paths from both the properties file and the JSON resource. Furthermore, If you want to validate the properties file only, you can use: ```kotlin config.from.enable(Feature.FAIL_ON_UNKNOWN_PATH).properties.file("/path/to/file") .from.json.resource("server.json") ``` ### Path substitution Path substitution is a feature that path references in source will be substituted by their values. Path substitution rules are shown below: - Only quoted string value will be substituted. It means that Konf's path substitutions will not conflict with HOCON's substitutions. - The definition of a path variable is `${path}`, e.g., `${java.version}`. - The path variable is resolved in the context of the current source. - If the string value only contains the path variable, it will be replaced by the whole sub-tree in the path; otherwise, it will be replaced by the string value in the path. - Use `${path:-default}` to provide a default value when the path is unresolved, e.g., `${java.version:-8}`. - Use `$${path}` to escape the path variable, e.g., `$${java.version}` will be resolved to `${java.version}` instead of the value in `java.version`. - Path substitution works in a recursive way, so nested path variables like `${jre-${java.specification.version}}` are allowed. - Konf also supports all key prefix of [StringSubstitutor](https://commons.apache.org/proper/commons-text/apidocs/org/apache/commons/text/StringSubstitutor.html)'s default interpolator. By default, Konf will perform path substitution for every source (except system environment source) when loading them into the config. You can disable this behaviour by using `config.disable(Feature.SUBSTITUTE_SOURCE_BEFORE_LOADED)` for the config or `source.disabled(Feature.SUBSTITUTE_SOURCE_BEFORE_LOADED)` for a single source. By default, Konf will throw exception when some path variables are unresolved. You can use `source.substituted(false)` manually to ignore these unresolved variables. To resolve path variables refer to other sources, you can merge these sources before loading them into the config. For example, if we have two sources `source1.json` and `source2.properties`, `source1.json` is: ```json { "base" : { "user" : "konf" , "password" : "passwd" } } ``` `source2.properties` is: ```properties connection.jdbc=mysql://${base.user}:${base.password}@server:port ``` use: ```kotlin config.withSource( Source.from.file("source1.json") + Source.from.file("source2.properties") ) ``` We can resolve `mysql://${base.user}:${base.password}@server:port` as `mysql://konf:passwd@server:port`. ## Prefix/Merge operations for source/config/config spec All of source/config/config spec support add prefix operation, remove prefix operation and merge operation as shown below: | Type | Add Prefix | Remove Prefix | Merge | | -------- | ------------------------------------------------------------ | ----------------------------------------------------------- | ------------------------------------------------------ | | `Source` | `source.withPrefix(prefix)` or `Prefix(prefix) + source` or `config.from.prefixed(prefix).file(file)` | `source[prefix]` or `config.from.scoped(prefix).file(file)` | `fallback + facade` or `facade.withFallback(fallback)` | | `Config` | `config.withPrefix(prefix)` or `Prefix(prefix) + config` | `config.at(prefix)` | `fallback + facade` or `facade.withFallback(fallback)` | | `Spec` | `spec.withPrefix(prefix)` or `Prefix(prefix) + spec` | `spec[prefix]` | `fallback + facade` or `facade.withFallback(fallback)` | ## Export/Reload values in config Export all values in config as a tree: ```kotlin val tree = config.toTree() ``` Export all values in config to map in key-value format: ```kotlin val map = config.toMap() ``` Export all values in config to hierarchical map: ```kotlin val map = config.toHierarchicalMap() ``` Export all values in config to map in flat format: ```kotlin val map = config.toFlatMap() ``` Export all values in config to JSON: ```kotlin val file = createTempFile(suffix = ".json") config.toJson.toFile(file) ``` Reload values from JSON: ```kotlin val newConfig = Config { addSpec(Server) }.from.json.file(file) check(config == newConfig) ``` Config can be saved to a variety of output format in HOCON/JSON/properties/TOML/XML/YAML/JavaScript. Use JSON as example: - To file: `config.toJson.toFile("/path/to/file")` - To string: `config.toJson.toText()` - To writer: `config.toJson.toWriter(writer)` - To output stream: `config.toJson.toOutputStream(outputStream)` - To byte array: `config.toJson.toBytes()` You can also implement [`Writer`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Writer.kt) to customize your new writer (see [`JsonWriter`](https://github.com/uchuhimo/konf/blob/master/konf-core/src/main/kotlin/com/uchuhimo/konf/source/json/JsonWriter.kt) for how to integrate your writer with config). ## Supported item types Supported item types include: - All primitive types - All primitive array types - `BigInteger` - `BigDecimal` - `String` - Date and Time - `java.util.Date` - `OffsetTime` - `OffsetDateTime` - `ZonedDateTime` - `LocalDate` - `LocalTime` - `LocalDateTime` - `Year` - `YearMonth` - `Instant` - `Duration` - `SizeInBytes` - Enum - Array - Collection - `List` - `Set` - `SortedSet` - `Map` - `SortedMap` - Kotlin Built-in classes - `Pair` - `Triple` - `IntRange` - `CharRange` - `LongRange` - Data classes - POJOs supported by Jackson core modules Konf supports size in bytes format described in [HOCON document](https://github.com/typesafehub/config/blob/master/HOCON.md#size-in-bytes-format) with class `SizeInBytes`. Konf supports both [ISO-8601 duration format](https://en.wikipedia.org/wiki/ISO_8601#Durations) and [HOCON duration format](https://github.com/typesafehub/config/blob/master/HOCON.md#duration-format) for `Duration`. Konf uses [Jackson](https://github.com/FasterXML/jackson) to support Kotlin Built-in classes, Data classes and POJOs. You can use `config.mapper` to access `ObjectMapper` instance used by config, and configure it to support more types from third-party Jackson modules. Default modules registered by Konf include: - Jackson core modules - `JavaTimeModule` in [jackson-modules-java8](https://github.com/FasterXML/jackson-modules-java8) - [jackson-module-kotlin](https://github.com/FasterXML/jackson-module-kotlin) ## Optional features There are some optional features that you can enable/disable in the config scope or the source scope by `Config#enable(Feature)`/`Config#disable(Feature)` or `Source#enabled(Feature)`/`Source#disable(Feature)`. You can use `Config#isEnabled()` or `Source#isEnabled()` to check whether a feature is enabled. These features include: - `FAIL_ON_UNKNOWN_PATH`: feature that determines what happens when unknown paths appear in the source. If enabled, an exception is thrown when loading from the source to indicate it contains unknown paths. This feature is disabled by default. - `LOAD_KEYS_CASE_INSENSITIVELY`: feature that determines whether loading keys from sources case-insensitively. This feature is disabled by default except for system environment. - `LOAD_KEYS_AS_LITTLE_CAMEL_CASE`: feature that determines whether loading keys from sources as little camel case. This feature is enabled by default. - `OPTIONAL_SOURCE_BY_DEFAULT`: feature that determines whether sources are optional by default. This feature is disabled by default. - `SUBSTITUTE_SOURCE_BEFORE_LOADED`: feature that determines whether sources should be substituted before loaded into config. This feature is enabled by default. ## Build from source Build library with Gradle using the following command: ``` ./gradlew clean assemble ``` Test library with Gradle using the following command: ``` ./gradlew clean test ``` Since Gradle has excellent incremental build support, you can usually omit executing the `clean` task. Install library in a local Maven repository for consumption in other projects via the following command: ``` ./gradlew clean install ``` ## Breaking Changes ### v0.19.0 Since all sources are substituted before loaded into config by default, all path variables will be substituted now. You can use `config.disable(Feature.SUBSTITUTE_SOURCE_BEFORE_LOADED)` to disable this change. ### v0.17.0 After migrated to tree-based source APIs, many deprecated APIs are removed, including: - `Source`: all `isXXX` and `toXXX` APIs - `Config`: `layer`, `addSource` and `withSourceFrom` ### v0.15 After modularized Konf, `hocon`/`toml`/`xml`/`yaml`/`git`/`watchGit` in `DefaultLoaders` become extension properties/functions and should be imported explicitly. For example, you should import `com.uchuhimo.konf.source.hocon` before using `config.from.hocon`; in Java, `config.from().hocon` is unavailable, please use `config.from().source(HoconProvider.INSTANCE)` instead. If you use JitPack, you should use `com.github.uchuhimo.konf:konf:` instead of `com.github.uchuhimo:konf:` now. ### v0.10 APIs in `ConfigSpec` have been updated to support item name's auto-detection, please migrate to new APIs. Here are some examples: - `val host = optional("host", "0.0.0.0")` to `val host by optional("0.0.0.0")` - `val port = required("port")` to `val port by required()` - `val nextPort = lazy("nextPort") { config -> config[port] + 1 }` to `val nextPort by lazy { config -> config[port] + 1 }` # License © uchuhimo, 2017-2019. Licensed under an [Apache 2.0](./LICENSE) license. ================================================ FILE: build.gradle.kts ================================================ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL import java.util.Properties val ossUserToken by extra { getPrivateProperty("ossUserToken") } val ossUserPassword by extra { getPrivateProperty("ossUserPassword") } val signPublications by extra { getPrivateProperty("signPublications") } val useAliyun by extra { shouldUseAliyun() } tasks.named("wrapper") { gradleVersion = "7.0" distributionType = Wrapper.DistributionType.ALL } buildscript { repositories { if (shouldUseAliyun()) { aliyunMaven() } else { mavenCentral() } } } plugins { java `java-test-fixtures` jacoco `maven-publish` signing kotlin("jvm") version Versions.kotlin kotlin("plugin.allopen") version Versions.kotlin id("com.dorongold.task-tree") version Versions.taskTree id("me.champeau.gradle.jmh") version Versions.jmhPlugin id("com.diffplug.spotless") version Versions.spotless id("com.github.ben-manes.versions") version Versions.dependencyUpdate id("org.jetbrains.dokka") version Versions.dokka } allprojects { apply(plugin = "java") apply(plugin = "java-test-fixtures") apply(plugin = "jacoco") apply(plugin = "maven-publish") apply(plugin = "signing") apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "kotlin-allopen") apply(plugin = "com.dorongold.task-tree") apply(plugin = "me.champeau.gradle.jmh") apply(plugin = "com.diffplug.spotless") apply(plugin = "com.github.ben-manes.versions") apply(plugin = "org.jetbrains.dokka") group = "com.uchuhimo" version = "1.1.2" repositories { if (useAliyun) { aliyunMaven() } else { mavenCentral() } maven { url=uri("https://kotlin.bintray.com/kotlinx") } } val dependencyUpdates by tasks.existing(DependencyUpdatesTask::class) dependencyUpdates { revision = "release" outputFormatter = "plain" resolutionStrategy { componentSelection { all { val rejected = listOf("alpha", "beta", "rc", "cr", "m", "preview", "b", "ea", "eap", "pr", "dev", "mt") .map { qualifier -> Regex("(?i).*[.-]$qualifier[.\\d-+]*") } .any { it.matches(candidate.version) } if (rejected) { reject("Release candidate") } } } } } } subprojects { configurations.testFixturesImplementation.get().extendsFrom(configurations.implementation.get()) configurations.testImplementation.get().extendsFrom(configurations.testFixturesImplementation.get()) dependencies { api(kotlin("stdlib-jdk8", Versions.kotlin)) api("org.jetbrains.kotlinx", "kotlinx-coroutines-core", Versions.coroutines) implementation(kotlin("reflect", Versions.kotlin)) implementation("org.reflections", "reflections", Versions.reflections) implementation("org.apache.commons", "commons-text", Versions.commonsText) arrayOf("core", "annotations", "databind").forEach { name -> api(jacksonCore(name, Versions.jackson)) } implementation(jackson("module", "kotlin", Versions.jackson)) implementation(jackson("datatype", "jsr310", Versions.jackson)) testFixturesImplementation(kotlin("test", Versions.kotlin)) testFixturesImplementation("com.natpryce", "hamkrest", Versions.hamkrest) testFixturesImplementation("org.hamcrest", "hamcrest-all", Versions.hamcrest) testImplementation(junit("jupiter", "api", Versions.junit)) testImplementation("com.sparkjava", "spark-core", Versions.spark) arrayOf("api", "data-driven-extension", "subject-extension").forEach { name -> testFixturesImplementation(spek(name, Versions.spek)) } testRuntimeOnly(junit("platform", "launcher", Versions.junitPlatform)) testRuntimeOnly(junit("jupiter", "engine", Versions.junit)) testRuntimeOnly(spek("junit-platform-engine", Versions.spek)) testRuntimeOnly("org.slf4j", "slf4j-simple", Versions.slf4j) } java { sourceCompatibility = Versions.java targetCompatibility = Versions.java } val test by tasks.existing(Test::class) test { useJUnitPlatform() testLogging.apply { showStandardStreams = true showExceptions = true showCauses = true showStackTraces = true } systemProperties["org.slf4j.simpleLogger.defaultLogLevel"] = "warn" systemProperties["junit.jupiter.execution.parallel.enabled"] = true systemProperties["junit.jupiter.execution.parallel.mode.default"] = "concurrent" maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1 val properties = Properties() properties.load(rootProject.file("konf-core/src/test/kotlin/com/uchuhimo/konf/source/env/env.properties").inputStream()) properties.forEach { key, value -> environment(key as String, value) } } tasks.withType { options.encoding = "UTF-8" } tasks.withType { kotlinOptions { jvmTarget = Versions.java.toString() apiVersion = Versions.kotlinApi languageVersion = Versions.kotlinLanguage } } allOpen { annotation("org.openjdk.jmh.annotations.BenchmarkMode") annotation("org.openjdk.jmh.annotations.State") } jmh { //jvmArgs = ["-Djmh.separateClasspathJAR=true"] iterations = 10 // Number of measurement iterations to do. //benchmarkMode = ["thrpt"] // Benchmark mode. Available modes are: [Throughput/thrpt, AverageTime/avgt, SampleTime/sample, SingleShotTime/ss, All/all] batchSize = 1 // Batch size: number of benchmark method calls per operation. (some benchmark modes can ignore this setting) fork = 1 // How many times to forks a single benchmark. Use 0 to disable forking altogether //operationsPerInvocation = 1 // Operations per invocation. timeOnIteration = "1s" // Time to spend at each measurement iteration. threads = 4 // Number of worker threads to run with. timeout = "10s" // Timeout for benchmark iteration. //timeUnit = "ns" // Output time unit. Available time units are: [m, s, ms, us, ns]. verbosity = "NORMAL" // Verbosity mode. Available modes are: [SILENT, NORMAL, EXTRA] warmup = "1s" // Time to spend at each warmup iteration. warmupBatchSize = 1 // Warmup batch size: number of benchmark method calls per operation. //warmupForks = 0 // How many warmup forks to make for a single benchmark. 0 to disable warmup forks. warmupIterations = 10 // Number of warmup iterations to do. isZip64 = false // Use ZIP64 format for bigger archives jmhVersion = Versions.jmh // Specifies JMH version } spotless { java { googleJavaFormat(Versions.googleJavaFormat) trimTrailingWhitespace() endWithNewline() licenseHeaderFile(rootProject.file("config/spotless/apache-license-2.0.java")) } kotlin { ktlint(Versions.ktlint) trimTrailingWhitespace() endWithNewline() licenseHeaderFile(rootProject.file("config/spotless/apache-license-2.0.kt")) } } jacoco { toolVersion = Versions.jacoco } val jacocoTestReport by tasks.existing(JacocoReport::class) { reports { xml.isEnabled = true html.isEnabled = true } } val check by tasks.existing { dependsOn(jacocoTestReport) } tasks.dokkaHtml { outputDirectory.set(tasks.javadoc.get().destinationDir) dokkaSourceSets { configureEach { jdkVersion.set(9) reportUndocumented.set(false) sourceLink { localDirectory.set(file("./")) remoteUrl.set(URL("https://github.com/uchuhimo/konf/blob/v${project.version}/")) remoteLineSuffix.set("#L") } } } } val sourcesJar by tasks.registering(Jar::class) { archiveClassifier.set("sources") from(sourceSets.main.get().allSource) } val javadocJar by tasks.registering(Jar::class) { archiveClassifier.set("javadoc") from(tasks.dokkaHtml) } val projectDescription = "A type-safe cascading configuration library for Kotlin/Java, " + "supporting most configuration formats" val projectGroup = project.group as String val projectName = if (project.name == "konf-all") "konf" else project.name val projectVersion = project.version as String val projectUrl = "https://github.com/uchuhimo/konf" publishing { publications { create("maven") { from(components["java"]) artifact(sourcesJar.get()) artifact(javadocJar.get()) groupId = projectGroup artifactId = projectName version = projectVersion suppressPomMetadataWarningsFor("testFixturesApiElements") suppressPomMetadataWarningsFor("testFixturesRuntimeElements") pom { name.set(rootProject.name) description.set(projectDescription) url.set(projectUrl) licenses { license { name.set("The Apache Software License, Version 2.0") url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") } } developers { developer { id.set("uchuhimo") name.set("uchuhimo") email.set("uchuhimo@outlook.com") } } scm { url.set(projectUrl) } } } } repositories { maven { url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2") credentials { username = ossUserToken password = ossUserPassword } } } } signing { setRequired({ signPublications == "true" }) sign(publishing.publications["maven"]) } tasks { val install by registering afterEvaluate { val publishToMavenLocal by existing val publish by existing install.configure { dependsOn(publishToMavenLocal) } publish { dependsOn(check, install) } } } } ================================================ FILE: buildSrc/build.gradle.kts ================================================ plugins { `kotlin-dsl` } repositories { mavenCentral() } ================================================ FILE: buildSrc/settings.gradle.kts ================================================ ================================================ FILE: buildSrc/src/main/kotlin/Dependencies.kt ================================================ import org.gradle.api.JavaVersion object Versions { val java = JavaVersion.VERSION_1_8 const val commonsText = "1.9" const val coroutines = "1.4.3" const val dependencyUpdate = "0.38.0" const val dokka = "1.4.30" const val dom4j = "2.1.3" const val graal = "21.0.0.2" const val hamcrest = "1.3" const val hamkrest = "1.8.0.1" const val hocon = "1.4.1" const val jacksonMinor = "2.12" const val jackson = "$jacksonMinor.2" const val jacoco = "0.8.6" const val jaxen = "1.2.0" const val jgit = "5.11.0.202103091610-r" const val jmh = "1.25.2" const val jmhPlugin = "0.5.3" const val junit = "5.7.1" const val junitPlatform = "1.7.1" const val kotlin = "1.4.32" const val kotlinApi = "1.4" const val kotlinLanguage = "1.4" const val reflections = "0.9.12" const val slf4j = "1.7.30" const val spark = "2.9.3" const val spek = "1.1.5" const val spotless = "5.11.1" const val taskTree = "1.5" const val toml4j = "0.7.2" const val yaml = "1.28" // Since 1.8, the minimum supported runtime version is JDK 11. const val googleJavaFormat = "1.7" const val ktlint = "0.41.0" } fun String?.withColon() = this?.let { ":$this" } ?: "" fun kotlin(module: String, version: String? = null) = "org.jetbrains.kotlin:kotlin-$module${version.withColon()}" fun spek(module: String, version: String? = null) = "org.jetbrains.spek:spek-$module${version.withColon()}" fun jackson(scope: String, module: String, version: String? = null) = "com.fasterxml.jackson.$scope:jackson-$scope-$module${version.withColon()}" fun jacksonCore(module: String = "core", version: String? = null) = "com.fasterxml.jackson.core:jackson-$module${version.withColon()}" fun junit(scope: String, module: String, version: String? = null) = "org.junit.$scope:junit-$scope-$module${version.withColon()}" ================================================ FILE: buildSrc/src/main/kotlin/Utils.kt ================================================ import org.gradle.api.Project import org.gradle.api.artifacts.dsl.RepositoryHandler import org.gradle.kotlin.dsl.maven import org.w3c.dom.Element import java.util.Properties fun Project.getPrivateProperty(key: String): String { return if (file("private.properties").exists()) { val properties = Properties() properties.load(file("private.properties").inputStream()) properties.getProperty(key) } else { "" } } fun Project.shouldUseAliyun(): Boolean = if (file("private.properties").exists()) { val properties = Properties() properties.load(file("private.properties").inputStream()) properties.getProperty("useAliyun")?.toBoolean() ?: false } else { false } fun RepositoryHandler.aliyunMaven() = maven(url = "https://maven.aliyun.com/repository/central") fun RepositoryHandler.aliyunGradlePluginPortal() = maven(url = "https://maven.aliyun.com/repository/gradle-plugin") fun Element.appendNode(key: String, action: Element.() -> Unit): Element { return apply { appendChild(ownerDocument.createElement(key).apply { action() }) } } fun Element.appendNode(key: String, value: String): Element { return appendNode(key) { appendChild(ownerDocument.createTextNode(value)) } } ================================================ FILE: config/spotless/apache-license-2.0.java ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ ================================================ FILE: config/spotless/apache-license-2.0.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en_US org.gradle.caching=true org.gradle.vfs.watch=true #org.gradle.parallel=true #org.gradle.configureondemand=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: konf-all/build.gradle.kts ================================================ sourceSets { register("snippet") } val snippetImplementation by configurations snippetImplementation.extendsFrom(configurations.implementation.get()) dependencies { for (name in listOf( ":konf-core", ":konf-git", ":konf-hocon", ":konf-toml", ":konf-xml", ":konf-yaml" )) { api(project(name)) testImplementation(testFixtures(project(name))) } snippetImplementation(sourceSets.main.get().output) val snippet by sourceSets testImplementation(snippet.output) } ================================================ FILE: konf-all/src/snippet/java/com/uchuhimo/konf/snippet/ServerInJava.java ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.snippet; import com.uchuhimo.konf.Config; public class ServerInJava { private String host; private Integer tcpPort; public ServerInJava(String host, Integer tcpPort) { this.host = host; this.tcpPort = tcpPort; } public ServerInJava(Config config) { this(config.get(ServerSpecInJava.host), config.get(ServerSpecInJava.tcpPort)); } public String getHost() { return host; } public Integer getTcpPort() { return tcpPort; } } ================================================ FILE: konf-all/src/snippet/java/com/uchuhimo/konf/snippet/ServerSpecInJava.java ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.snippet; import com.uchuhimo.konf.ConfigSpec; import com.uchuhimo.konf.OptionalItem; import com.uchuhimo.konf.RequiredItem; public class ServerSpecInJava { public static final ConfigSpec spec = new ConfigSpec("server"); public static final OptionalItem host = new OptionalItem(spec, "host", "0.0.0.0") {}; public static final RequiredItem tcpPort = new RequiredItem(spec, "tcpPort") {}; } ================================================ FILE: konf-all/src/snippet/kotlin/com/uchuhimo/konf/snippet/Config.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.snippet import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec fun main(args: Array) { val config = Config() config.addSpec(Server) run { val host = config[Server.host] } run { val host = config.get("server.host") } run { val host = config("server.host") } config.contains(Server.host) // or Server.host in config config.contains("server.host") // or "server.host" in config config[Server.tcpPort] = 80 config["server.tcpPort"] = 80 config.containsRequired() config.validateRequired() config.unset(Server.tcpPort) config.unset("server.tcpPort") val basePort by ConfigSpec("server").required() config.lazySet(Server.tcpPort) { it[basePort] + 1 } config.lazySet("server.tcpPort") { it[basePort] + 1 } run { val handler = Server.host.onSet { value -> println("the host has changed to $value") } handler.cancel() } run { val handler = Server.host.beforeSet { config, value -> println("the host will change to $value") } handler.cancel() } run { val handler = config.beforeSet { item, value -> println("${item.name} will change to $value") } handler.cancel() } run { val handler = Server.host.afterSet { config, value -> println("the host has changed to $value") } handler.cancel() } run { val handler = config.afterSet { item, value -> println("${item.name} has changed to $value") } handler.cancel() } run { var port by config.property(Server.tcpPort) port = 9090 check(port == 9090) } run { val port by config.property(Server.tcpPort) check(port == 9090) } } ================================================ FILE: konf-all/src/snippet/kotlin/com/uchuhimo/konf/snippet/Export.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.snippet import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.base.toFlatMap import com.uchuhimo.konf.source.base.toHierarchicalMap import com.uchuhimo.konf.source.json.toJson import com.uchuhimo.konf.tempFile fun main(args: Array) { val config = Config { addSpec(Server) } config[Server.tcpPort] = 1000 run { val map = config.toMap() } run { val map = config.toHierarchicalMap() } run { val map = config.toFlatMap() } val file = tempFile(suffix = ".json") config.toJson.toFile(file) val newConfig = Config { addSpec(Server) }.from.json.file(file) check(config.toMap() == newConfig.toMap()) } ================================================ FILE: konf-all/src/snippet/kotlin/com/uchuhimo/konf/snippet/Fork.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.snippet import com.uchuhimo.konf.Config fun main(args: Array) { val config = Config { addSpec(Server) } config[Server.tcpPort] = 1000 // fork from parent config val childConfig = config.withLayer("child") // child config inherit values from parent config check(childConfig[Server.tcpPort] == 1000) // modifications in parent config affect values in child config config[Server.tcpPort] = 2000 check(config[Server.tcpPort] == 2000) check(childConfig[Server.tcpPort] == 2000) // modifications in child config don't affect values in parent config childConfig[Server.tcpPort] = 3000 check(config[Server.tcpPort] == 2000) check(childConfig[Server.tcpPort] == 3000) } ================================================ FILE: konf-all/src/snippet/kotlin/com/uchuhimo/konf/snippet/Load.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.snippet import com.uchuhimo.konf.Config fun main(args: Array) { val config = Config { addSpec(Server) } // values in source is loaded into new layer in child config val childConfig = config.from.env() check(childConfig.parent === config) } ================================================ FILE: konf-all/src/snippet/kotlin/com/uchuhimo/konf/snippet/QuickStart.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.snippet import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.toValue import com.uchuhimo.konf.source.yaml import com.uchuhimo.konf.toValue import java.io.File object ServerSpec : ConfigSpec() { val host by optional("0.0.0.0") val tcpPort by required() } fun main(args: Array) { val file = File("server.yml") //language=YAML file.writeText( """ server: host: 127.0.0.1 tcp_port: 8080 """.trimIndent() ) file.deleteOnExit() val config = Config { addSpec(ServerSpec) } .from.yaml.file("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() run { val config = Config { addSpec(ServerSpec) }.withSource( Source.from.yaml.file("server.yml") + Source.from.json.resource("server.json") + Source.from.env() + Source.from.systemProperties() ) } run { val config = Config { addSpec(ServerSpec) } .from.yaml.watchFile("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() } val server = Server(config[ServerSpec.host], config[ServerSpec.tcpPort]) server.start() run { val server = Config() .from.yaml.file("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() .at("server") .toValue() server.start() } run { val server = ( Source.from.yaml.file("server.yml") + Source.from.json.resource("server.json") + Source.from.env() + Source.from.systemProperties() )["server"] .toValue() server.start() } } ================================================ FILE: konf-all/src/snippet/kotlin/com/uchuhimo/konf/snippet/Serialize.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.snippet import com.uchuhimo.konf.Config import com.uchuhimo.konf.tempFile import java.io.ObjectInputStream import java.io.ObjectOutputStream fun main(args: Array) { val config = Config { addSpec(Server) } config[Server.tcpPort] = 1000 val map = config.toMap() val newMap = tempFile().run { ObjectOutputStream(outputStream()).use { it.writeObject(map) } ObjectInputStream(inputStream()).use { @Suppress("UNCHECKED_CAST") it.readObject() as Map } } val newConfig = Config { addSpec(Server) }.from.map.kv(newMap) check(config.toMap() == newConfig.toMap()) } ================================================ FILE: konf-all/src/snippet/kotlin/com/uchuhimo/konf/snippet/Server.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.snippet import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec data class Server(val host: String, val tcpPort: Int) { constructor(config: Config) : this(config[Server.host], config[Server.tcpPort]) fun start() {} companion object : ConfigSpec("server") { val host by optional("0.0.0.0", description = "host IP of server") val tcpPort by required(description = "port of server") val nextPort by lazy { config -> config[tcpPort] + 1 } } } ================================================ FILE: konf-all/src/snippet/resources/server.json ================================================ { "server": { "host": "127.0.0.1", "tcp_port": 8080 } } ================================================ FILE: konf-all/src/test/kotlin/com/uchuhimo/konf/source/MergeSourcesWithDifferentFeaturesSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on object MergeSourcesWithDifferentFeaturesSpec : Spek({ on("load from merged sources") { val config = Config { addSpec(ServicingConfig) }.withSource( Source.from.hocon.string(content) + Source.from.env() ) it("should contain the item") { assertThat(config[ServicingConfig.baseURL], equalTo("https://service/api")) assertThat(config[ServicingConfig.url], equalTo("https://service/api/index.html")) } } }) object ServicingConfig : ConfigSpec("servicing") { val baseURL by required() val url by required() } val content = """ servicing { baseURL = "https://service/api" url = "${'$'}{servicing.baseURL}/index.html" } """.trimIndent() ================================================ FILE: konf-all/src/test/kotlin/com/uchuhimo/konf/source/MultiLayerConfigToValueSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.fasterxml.jackson.databind.DeserializationFeature import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.toValue import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on object MultiLayerConfigToValueSpec : Spek({ val yamlContent = """ db: driverClassName: org.h2.Driver url: 'jdbc:h2:mem:db;DB_CLOSE_DELAY=-1' """.trimIndent() val map = mapOf( "driverClassName" to "org.h2.Driver", "url" to "jdbc:h2:mem:db;DB_CLOSE_DELAY=-1" ) on("load from multiple sources") { val config = Config { mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) } .from.yaml.string(yamlContent) .from.yaml.file( System.getenv("SERVICE_CONFIG") ?: "/opt/legacy-event-service/conf/legacy-event-service.yml", true ) .from.systemProperties() .from.env() it("should cast to value correctly") { val db = config.toValue() assertThat(db.db, equalTo(map)) } } }) data class ConfigTestReport(val db: Map) ================================================ FILE: konf-all/src/test/kotlin/com/uchuhimo/konf/source/MultipleDefaultLoadersSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on object MultipleDefaultLoadersSpec : Spek({ on("load from multiple sources") { val config = Config { addSpec(DefaultLoadersConfig) } val item = DefaultLoadersConfig.type val afterLoadEnv = config.from.env() System.setProperty(config.nameOf(DefaultLoadersConfig.type), "system") val afterLoadSystemProperties = afterLoadEnv.from.systemProperties() val afterLoadHocon = afterLoadSystemProperties.from.hocon.string(hoconContent) val afterLoadJson = afterLoadHocon.from.json.string(jsonContent) val afterLoadProperties = afterLoadJson.from.properties.string(propertiesContent) val afterLoadToml = afterLoadProperties.from.toml.string(tomlContent) val afterLoadXml = afterLoadToml.from.xml.string(xmlContent) val afterLoadYaml = afterLoadXml.from.yaml.string(yamlContent) val afterLoadFlat = afterLoadYaml.from.map.flat(mapOf("source.test.type" to "flat")) val afterLoadKv = afterLoadFlat.from.map.kv(mapOf("source.test.type" to "kv")) val afterLoadHierarchical = afterLoadKv.from.map.hierarchical( mapOf( "source" to mapOf( "test" to mapOf("type" to "hierarchical") ) ) ) it("should load the corresponding value in each layer") { assertThat(afterLoadEnv[item], equalTo("env")) assertThat(afterLoadSystemProperties[item], equalTo("system")) assertThat(afterLoadHocon[item], equalTo("conf")) assertThat(afterLoadJson[item], equalTo("json")) assertThat(afterLoadProperties[item], equalTo("properties")) assertThat(afterLoadToml[item], equalTo("toml")) assertThat(afterLoadXml[item], equalTo("xml")) assertThat(afterLoadYaml[item], equalTo("yaml")) assertThat(afterLoadFlat[item], equalTo("flat")) assertThat(afterLoadKv[item], equalTo("kv")) assertThat(afterLoadHierarchical[item], equalTo("hierarchical")) } } }) //language=Json const val jsonContent = """ { "source": { "test": { "type": "json" } } } """ ================================================ FILE: konf-all/src/test/kotlin/com/uchuhimo/konf/source/QuickStartSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.snippet.Server import com.uchuhimo.konf.snippet.ServerSpec import com.uchuhimo.konf.toValue import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import java.io.File object QuickStartSpec : Spek({ on("use default loaders") { val config = useFile { Config { addSpec(ServerSpec) } .from.yaml.file("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() } it("should load all values") { assertThat( config.toMap(), equalTo(mapOf("server.host" to "127.0.0.1", "server.tcpPort" to 8080)) ) } } on("use default providers") { val config = useFile { Config { addSpec(ServerSpec) }.withSource( Source.from.yaml.file("server.yml") + Source.from.json.resource("server.json") + Source.from.env() + Source.from.systemProperties() ) } it("should load all values") { assertThat( config.toMap(), equalTo(mapOf("server.host" to "127.0.0.1", "server.tcpPort" to 8080)) ) } } on("watch file") { val config = useFile { Config { addSpec(ServerSpec) } .from.yaml.watchFile("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() } it("should load all values") { assertThat( config.toMap(), equalTo(mapOf("server.host" to "127.0.0.1", "server.tcpPort" to 8080)) ) } } on("cast config to value") { val config = useFile { Config() .from.yaml.file("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() .at("server") } val server = config.toValue() it("should load all values") { assertThat(server, equalTo(Server(host = "127.0.0.1", tcpPort = 8080))) } } on("cast source to value") { val source = useFile { ( Source.from.yaml.file("server.yml") + Source.from.json.resource("server.json") + Source.from.env() + Source.from.systemProperties() )["server"] } val server = source.toValue() it("should load all values") { assertThat(server, equalTo(Server(host = "127.0.0.1", tcpPort = 8080))) } } }) private fun useFile(block: () -> T): T { val file = File("server.yml") //language=YAML file.writeText( """ server: host: 127.0.0.1 tcp_port: 8080 """.trimIndent() ) try { return block() } finally { file.delete() } } ================================================ FILE: konf-core/build.gradle.kts ================================================ dependencies { jmhImplementation(kotlin("stdlib", Versions.kotlin)) } ================================================ FILE: konf-core/src/jmh/kotlin/com/uchuhimo/konf/ConfigBenchmark.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode import org.openjdk.jmh.annotations.Mode.AverageTime import org.openjdk.jmh.annotations.OutputTimeUnit import org.openjdk.jmh.annotations.Scope import org.openjdk.jmh.annotations.State import java.util.concurrent.TimeUnit.NANOSECONDS class Buffer { companion object : ConfigSpec("network.buffer") { val name by optional("buffer", description = "name of buffer") } } @BenchmarkMode(AverageTime) @OutputTimeUnit(NANOSECONDS) class ConfigBenchmark { @State(Scope.Thread) class ConfigState { val config = Config { addSpec(Buffer) } val path = Buffer.qualify(Buffer.name) } @State(Scope.Benchmark) class MultiThreadConfigState { val config = Config { addSpec(Buffer) } val path = Buffer.qualify(Buffer.name) } @Benchmark fun getWithItem(state: ConfigState) = state.config[Buffer.name] @Benchmark fun getWithItemFromMultiThread(state: MultiThreadConfigState) = state.config[Buffer.name] @Benchmark fun setWithItem(state: ConfigState) { state.config[Buffer.name] = "newName" } @Benchmark fun setWithItemFromMultiThread(state: MultiThreadConfigState) { state.config[Buffer.name] = "newName" } @Benchmark fun getWithPath(state: ConfigState) = state.config(state.path) @Benchmark fun getWithPathFromMultiThread(state: MultiThreadConfigState) = state.config(state.path) @Benchmark fun setWithPath(state: ConfigState) { state.config[state.path] = "newName" } @Benchmark fun setWithPathFromMultiThread(state: MultiThreadConfigState) { state.config[state.path] = "newName" } } @BenchmarkMode(AverageTime) @OutputTimeUnit(NANOSECONDS) class MultiLevelConfigBenchmark { @State(Scope.Thread) class ConfigState { val config = Config { addSpec(Buffer) }.withLayer().withLayer().withLayer().withLayer() val path = Buffer.qualify(Buffer.name) } @State(Scope.Benchmark) class MultiThreadConfigState { val config = Config { addSpec(Buffer) }.withLayer().withLayer().withLayer().withLayer() val path = Buffer.qualify(Buffer.name) } @Benchmark fun getWithItem(state: ConfigState) = state.config[Buffer.name] @Benchmark fun getWithItemFromMultiThread(state: MultiThreadConfigState) = state.config[Buffer.name] @Benchmark fun setWithItem(state: ConfigState) { state.config[Buffer.name] = "newName" } @Benchmark fun setWithItemFromMultiThread(state: MultiThreadConfigState) { state.config[Buffer.name] = "newName" } @Benchmark fun getWithPath(state: ConfigState) = state.config(state.path) @Benchmark fun getWithPathFromMultiThread(state: MultiThreadConfigState) = state.config(state.path) @Benchmark fun setWithPath(state: ConfigState) { state.config[state.path] = "newName" } @Benchmark fun setWithPathFromMultiThread(state: MultiThreadConfigState) { state.config[state.path] = "newName" } } ================================================ FILE: konf-core/src/main/java/com/uchuhimo/konf/Configs.java ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf; import java.util.function.Consumer; /** Helper class for {@link com.uchuhimo.konf.Config Config}. */ public final class Configs { private Configs() {} /** * Create a new root config. * * @return a new root config */ public static Config create() { return Config.Companion.invoke(); } /** * Create a new root config and initiate it. * * @param init initial action * @return a new root config */ public static Config create(Consumer init) { final Config config = create(); init.accept(config); return config; } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/BaseConfig.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.cfg.CoercionAction import com.fasterxml.jackson.databind.cfg.CoercionInputShape import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.uchuhimo.konf.source.MergedSource import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.base.EmptyMapSource import com.uchuhimo.konf.source.deserializer.DurationDeserializer import com.uchuhimo.konf.source.deserializer.EmptyStringToCollectionDeserializerModifier import com.uchuhimo.konf.source.deserializer.OffsetDateTimeDeserializer import com.uchuhimo.konf.source.deserializer.StringDeserializer import com.uchuhimo.konf.source.deserializer.ZoneDateTimeDeserializer import com.uchuhimo.konf.source.load import com.uchuhimo.konf.source.loadItem import com.uchuhimo.konf.source.toCompatibleValue import java.time.Duration import java.time.OffsetDateTime import java.time.ZonedDateTime import java.util.WeakHashMap import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read import kotlin.concurrent.write import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty /** * The default implementation for [Config]. */ open class BaseConfig( override val name: String = "", override val parent: BaseConfig? = null, override val mapper: ObjectMapper = createDefaultMapper(), private val specsInLayer: MutableList = mutableListOf(), private val featuresInLayer: MutableMap = mutableMapOf(), private val nodeByItem: MutableMap, ItemNode> = mutableMapOf(), private val tree: TreeNode = ContainerNode.placeHolder(), private val hasChildren: Value = Value(false), private val beforeSetFunctions: MutableList<(item: Item<*>, value: Any?) -> Unit> = mutableListOf(), private val afterSetFunctions: MutableList<(item: Item<*>, value: Any?) -> Unit> = mutableListOf(), private val beforeLoadFunctions: MutableList<(source: Source) -> Unit> = mutableListOf(), private val afterLoadFunctions: MutableList<(source: Source) -> Unit> = mutableListOf(), private val lock: ReentrantReadWriteLock = ReentrantReadWriteLock() ) : Config { private val _source: Value = Value(EmptyMapSource()) open val source: Source get() = _source.value private val nameByItem: WeakHashMap, String> = WeakHashMap() override fun lock(action: () -> T): T = lock.write(action) override fun at(path: String): Config { if (path.isEmpty()) { return this } else { val originalConfig = this return object : BaseConfig( name = name, parent = parent?.at(path) as BaseConfig?, mapper = mapper, specsInLayer = specsInLayer, featuresInLayer = featuresInLayer, nodeByItem = nodeByItem, tree = tree.getOrNull(path) ?: ContainerNode.placeHolder().also { lock.write { tree[path] = it } }, hasChildren = hasChildren, beforeSetFunctions = beforeSetFunctions, afterSetFunctions = afterSetFunctions, beforeLoadFunctions = beforeLoadFunctions, afterLoadFunctions = afterLoadFunctions, lock = lock ) { override val source: Source get() { if (path !in originalConfig.source) { originalConfig.source.tree[path] = ContainerNode.placeHolder() } return originalConfig.source[path] } } } } override fun withPrefix(prefix: String): Config { if (prefix.isEmpty()) { return this } else { val originalConfig = this return object : BaseConfig( name = name, parent = parent?.withPrefix(prefix) as BaseConfig?, mapper = mapper, specsInLayer = specsInLayer, featuresInLayer = featuresInLayer, nodeByItem = nodeByItem, tree = if (prefix.isEmpty()) tree else ContainerNode.empty().apply { set(prefix, tree) }, hasChildren = hasChildren, beforeSetFunctions = beforeSetFunctions, afterSetFunctions = afterSetFunctions, beforeLoadFunctions = beforeLoadFunctions, afterLoadFunctions = afterLoadFunctions, lock = lock ) { override val source: Source get() = originalConfig.source.withPrefix(prefix) } } } override fun iterator(): Iterator> { return if (parent != null) { (nodeByItem.keys.iterator().asSequence() + parent!!.iterator().asSequence()).iterator() } else { nodeByItem.keys.iterator() } } override val itemWithNames: List, String>> get() = lock.read { tree.leafByPath }.map { (name, node) -> (node as ItemNode).item to name } + (parent?.itemWithNames ?: listOf()) override fun toMap(): Map { return lock.read { itemWithNames.map { (item, name) -> name to try { getOrNull(item, errorWhenNotFound = true).toCompatibleValue(mapper) } catch (_: UnsetValueException) { ValueState.Unset } }.filter { (_, value) -> value != ValueState.Unset }.toMap() } } @Suppress("UNCHECKED_CAST") override fun get(item: Item): T = getOrNull(item, errorWhenNotFound = true) as T @Suppress("UNCHECKED_CAST") override fun get(name: String): T = getOrNull(name, errorWhenNotFound = true) as T @Suppress("UNCHECKED_CAST") override fun getOrNull(item: Item): T? = getOrNull(item, errorWhenNotFound = false) as T? private fun setState(item: Item<*>, state: ValueState) { if (item in nodeByItem) { nodeByItem[item]!!.value = state } else { nodeByItem[item] = ItemNode(state, item) } } open fun getOrNull( item: Item<*>, errorWhenNotFound: Boolean, errorWhenGetDefault: Boolean = false, lazyContext: ItemContainer = this ): Any? { val valueState = lock.read { nodeByItem[item]?.value } if (valueState != null) { @Suppress("UNCHECKED_CAST") when (valueState) { is ValueState.Unset -> if (errorWhenNotFound) { throw UnsetValueException(item) } else { return null } is ValueState.Null -> return null is ValueState.Value -> return valueState.value is ValueState.Default -> { if (errorWhenGetDefault) { throw GetDefaultValueException(item) } else { return valueState.value } } is ValueState.Lazy<*> -> { val value = try { valueState.thunk(lazyContext) } catch (exception: ConfigException) { when (exception) { is UnsetValueException, is NoSuchItemException -> { if (errorWhenNotFound) { throw exception } else { return null } } else -> throw exception } } if (value == null) { if (item.nullable) { return null } else { throw InvalidLazySetException( "fail to cast null to ${item.type.rawClass}" + " when getting item ${item.name} in config" ) } } else { if (item.type.rawClass.isInstance(value)) { return value } else { throw InvalidLazySetException( "fail to cast $value with ${value::class} to ${item.type.rawClass}" + " when getting item ${item.name} in config" ) } } } } } else { if (parent != null) { return parent!!.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) } else { if (errorWhenNotFound) { throw NoSuchItemException(item) } else { return null } } } } open fun getItemOrNull(name: String): Item<*>? { val trimmedName = name.trim() val item = getItemInLayerOrNull(trimmedName) return item ?: parent?.getItemOrNull(trimmedName) } private fun getItemInLayerOrNull(name: String): Item<*>? { return lock.read { (tree.getOrNull(name) as? ItemNode)?.item } } @Suppress("UNCHECKED_CAST") override fun getOrNull(name: String): T? = getOrNull(name, errorWhenNotFound = false) as T? private fun getOrNull(name: String, errorWhenNotFound: Boolean): Any? { val item = getItemOrNull(name) return if (item != null) { getOrNull(item, errorWhenNotFound) } else { if (errorWhenNotFound) { throw NoSuchItemException(name) } else { null } } } private fun containsInLayer(item: Item<*>) = lock.read { nodeByItem.containsKey(item) } override fun contains(item: Item<*>): Boolean { return if (containsInLayer(item)) { true } else { parent?.contains(item) ?: false } } private fun containsInLayer(name: String): Boolean { return containsInLayer(name.toPath()) } override fun contains(name: String): Boolean { return if (containsInLayer(name)) { true } else { parent?.contains(name) ?: false } } private fun TreeNode.partialMatch(path: Path): Boolean { return if (this is LeafNode) { true } else if (path.isEmpty()) { !isEmpty() } else { val key = path.first() val rest = path.drop(1) val result = children[key] if (result != null) { return result.partialMatch(rest) } else { return false } } } private fun containsInLayer(path: Path): Boolean { return lock.read { tree.partialMatch(path) } } override fun contains(path: Path): Boolean = containsInLayer(path) || (parent?.contains(path) ?: false) override fun nameOf(item: Item<*>): String { return nameByItem[item] ?: { val name = lock.read { tree.firstPath { it is ItemNode && it.item == item } }?.name if (name != null) { nameByItem[item] = name name } else { parent?.nameOf(item) ?: throw NoSuchItemException(item) } }() } open fun addBeforeSetFunction(beforeSetFunction: (item: Item<*>, value: Any?) -> Unit) { beforeSetFunctions += beforeSetFunction parent?.addBeforeSetFunction(beforeSetFunction) } open fun removeBeforeSetFunction(beforeSetFunction: (item: Item<*>, value: Any?) -> Unit) { beforeSetFunctions.remove(beforeSetFunction) parent?.removeBeforeSetFunction(beforeSetFunction) } override fun beforeSet(beforeSetFunction: (item: Item<*>, value: Any?) -> Unit): Handler { addBeforeSetFunction(beforeSetFunction) return object : Handler { override fun cancel() { removeBeforeSetFunction(beforeSetFunction) } } } private fun notifyBeforeSet(item: Item<*>, value: Any?) { for (beforeSetFunction in beforeSetFunctions) { beforeSetFunction(item, value) } } open fun addAfterSetFunction(afterSetFunction: (item: Item<*>, value: Any?) -> Unit) { afterSetFunctions += afterSetFunction parent?.addAfterSetFunction(afterSetFunction) } open fun removeAfterSetFunction(afterSetFunction: (item: Item<*>, value: Any?) -> Unit) { afterSetFunctions.remove(afterSetFunction) parent?.removeAfterSetFunction(afterSetFunction) } override fun afterSet(afterSetFunction: (item: Item<*>, value: Any?) -> Unit): Handler { addAfterSetFunction(afterSetFunction) return object : Handler { override fun cancel() { removeAfterSetFunction(afterSetFunction) } } } private fun notifyAfterSet(item: Item<*>, value: Any?) { for (afterSetFunction in afterSetFunctions) { afterSetFunction(item, value) } } override fun rawSet(item: Item<*>, value: Any?) { if (item in this) { if (value == null) { if (item.nullable) { item.notifySet(null) item.notifyBeforeSet(this, value) notifyBeforeSet(item, value) lock.write { setState(item, ValueState.Null) } notifyAfterSet(item, value) item.notifyAfterSet(this, value) } else { throw ClassCastException( "fail to cast null to ${item.type.rawClass}" + " when setting item ${item.name} in config" ) } } else { if (item.type.rawClass.isInstance(value)) { item.notifySet(value) item.notifyBeforeSet(this, value) notifyBeforeSet(item, value) lock.write { setState(item, ValueState.Value(value)) } notifyAfterSet(item, value) item.notifyAfterSet(this, value) } else { throw ClassCastException( "fail to cast $value with ${value::class} to ${item.type.rawClass}" + " when setting item ${item.name} in config" ) } } } else { throw NoSuchItemException(item) } } override fun set(item: Item, value: T) { rawSet(item, value) } override fun set(name: String, value: T) { val item = getItemOrNull(name) if (item != null) { @Suppress("UNCHECKED_CAST") set(item as Item, value) } else { throw NoSuchItemException(name) } } override fun lazySet(item: Item, thunk: (config: ItemContainer) -> T) { if (item in this) { lock.write { setState(item, ValueState.Lazy(thunk)) } } else { throw NoSuchItemException(item) } } override fun lazySet(name: String, thunk: (config: ItemContainer) -> T) { val item = getItemOrNull(name) if (item != null) { @Suppress("UNCHECKED_CAST") lazySet(item as Item, thunk) } else { throw NoSuchItemException(name) } } override fun unset(item: Item<*>) { if (item in this) { lock.write { setState(item, ValueState.Unset) } } else { throw NoSuchItemException(item) } } override fun unset(name: String) { val item = getItemOrNull(name) if (item != null) { unset(item) } else { throw NoSuchItemException(name) } } override fun clear() { lock.write { nodeByItem.clear() tree.children.clear() if (tree is MapNode) { tree.isPlaceHolder = true } } } override fun clearAll() { clear() parent?.clearAll() } override fun containsRequired(): Boolean = try { validateRequired() true } catch (ex: UnsetValueException) { false } override fun validateRequired(): Config { for (item in this) { if (item is RequiredItem) { getOrNull(item, errorWhenNotFound = true) } } return this } override fun plus(config: Config): Config { return when (config) { is BaseConfig -> MergedConfig(this, config) else -> config.withFallback(this) } } override fun withFallback(config: Config): Config { return config + this } override fun property(item: Item): ReadWriteProperty { if (!contains(item)) { throw NoSuchItemException(item) } return object : ReadWriteProperty { override fun getValue(thisRef: Any?, property: KProperty<*>): T = get(item) override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = set(item, value) } } override fun property(name: String): ReadWriteProperty { if (!contains(name)) { throw NoSuchItemException(name) } return object : ReadWriteProperty { override fun getValue(thisRef: Any?, property: KProperty<*>): T = get(name) override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = set(name, value) } } override val specs: List get() = lock.read { specsInLayer + (parent?.specs ?: listOf()) } override val sources: List get() { return lock.read { mutableListOf(source) }.apply { for (source in parent?.sources ?: listOf()) { add(source) } } } override fun enable(feature: Feature): Config { return apply { lock.write { featuresInLayer[feature] = true } } } override fun disable(feature: Feature): Config { return apply { lock.write { featuresInLayer[feature] = false } } } override fun isEnabled(feature: Feature): Boolean { return lock.read { featuresInLayer[feature] ?: parent?.isEnabled(feature) ?: feature.enabledByDefault } } override fun addItem(item: Item<*>, prefix: String) { lock.write { if (hasChildren.value) { throw LayerFrozenException(this) } val path = prefix.toPath() + item.name.toPath() val name = path.name if (item !in this) { if (path in this) { throw NameConflictException("item $name cannot be added") } val node = ItemNode( when (item) { is OptionalItem -> ValueState.Default(item.default) is RequiredItem -> ValueState.Unset is LazyItem -> ValueState.Lazy(item.thunk) }, item ) tree[name] = node nodeByItem[item] = node val sources = this.sources val mergedSource = if (sources.isNotEmpty()) { sources.reduceRight { source, acc -> MergedSource(source, acc) } } else { null } mergedSource?.let { loadItem(item, path, it) } } else { throw RepeatedItemException(name) } } } override fun addSpec(spec: Spec) { lock.write { if (hasChildren.value) { throw LayerFrozenException(this) } val sources = this.sources val mergedSource = if (sources.isNotEmpty()) { sources.reduceRight { source, acc -> MergedSource(source, acc) } } else { null } spec.items.forEach { item -> val name = spec.qualify(item) if (item !in this) { val path = name.toPath() if (path in this) { throw NameConflictException("item $name cannot be added") } val node = ItemNode( when (item) { is OptionalItem -> ValueState.Default(item.default) is RequiredItem -> ValueState.Unset is LazyItem -> ValueState.Lazy(item.thunk) }, item ) tree[name] = node nodeByItem[item] = node mergedSource?.let { loadItem(item, path, it) } } else { throw RepeatedItemException(name) } } spec.innerSpecs.forEach { innerSpec -> addSpec(innerSpec.withPrefix(spec.prefix)) } specsInLayer += spec } } override fun withLayer(name: String): BaseConfig { lock.write { hasChildren.value = true } return BaseConfig(name, this, mapper) } open fun addBeforeLoadFunction(beforeLoadFunction: (source: Source) -> Unit) { beforeLoadFunctions += beforeLoadFunction parent?.addBeforeLoadFunction(beforeLoadFunction) } open fun removeBeforeLoadFunction(beforeLoadFunction: (source: Source) -> Unit) { beforeLoadFunctions.remove(beforeLoadFunction) parent?.removeBeforeLoadFunction(beforeLoadFunction) } override fun beforeLoad(beforeLoadFunction: (source: Source) -> Unit): Handler { addBeforeLoadFunction(beforeLoadFunction) return object : Handler { override fun cancel() { removeBeforeLoadFunction(beforeLoadFunction) } } } private fun notifyBeforeLoad(source: Source) { for (beforeLoadFunction in beforeLoadFunctions) { beforeLoadFunction(source) } } open fun addAfterLoadFunction(afterLoadFunction: (source: Source) -> Unit) { afterLoadFunctions += afterLoadFunction parent?.addAfterLoadFunction(afterLoadFunction) } open fun removeAfterLoadFunction(afterLoadFunction: (source: Source) -> Unit) { afterLoadFunctions.remove(afterLoadFunction) parent?.removeAfterLoadFunction(afterLoadFunction) } override fun afterLoad(afterLoadFunction: (source: Source) -> Unit): Handler { addAfterLoadFunction(afterLoadFunction) return object : Handler { override fun cancel() { removeAfterLoadFunction(afterLoadFunction) } } } private fun notifyAfterLoad(source: Source) { for (afterLoadFunction in afterLoadFunctions) { afterLoadFunction(source) } } override fun withSource(source: Source): Config { return withLayer("source: ${source.description}").also { config -> config.lock.write { config._source.value = load(config, source) } } } override fun withLoadTrigger( description: String, trigger: ( config: Config, load: (source: Source) -> Unit ) -> Unit ): Config { return withLayer("trigger: $description").apply { trigger(this) { source -> notifyBeforeLoad(source) lock.write { this._source.value = load(this, source) } notifyAfterLoad(source) } } } override fun toString(): String { return "Config(items=${toMap()})" } class ItemNode(override var value: ValueState, val item: Item<*>) : ValueNode data class Value(var value: T) sealed class ValueState { object Unset : ValueState() object Null : ValueState() data class Lazy(val thunk: (config: ItemContainer) -> T) : ValueState() data class Value(val value: Any) : ValueState() data class Default(val value: Any?) : ValueState() } } /** * Returns a new default object mapper for config. */ fun createDefaultMapper(): ObjectMapper = jacksonObjectMapper() .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) .enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) .apply { coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty) } .registerModules( SimpleModule() .addDeserializer(String::class.java, StringDeserializer) .setDeserializerModifier(EmptyStringToCollectionDeserializerModifier), JavaTimeModule() .addDeserializer(Duration::class.java, DurationDeserializer) .addDeserializer(OffsetDateTime::class.java, OffsetDateTimeDeserializer) .addDeserializer(ZonedDateTime::class.java, ZoneDateTimeDeserializer) ) ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/Config.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.type.TypeFactory import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.DefaultLoaders import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.base.kvToTree import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty /** * Config containing items and associated values. * * Config contains items, which can be loaded with [addSpec]. * Config contains values, each of which is associated with corresponding item. * Values can be loaded from [source][Source] with [withSource] or [from]. * * Config contains read-write access operations for item. * Items in config is in one of three states: * - Unset. Item has not associated value in this state. * Use [unset] to change item to this state. * - Unevaluated. Item is lazy and the associated value will be evaluated when accessing. * Use [lazySet] to change item to this state. * - Evaluated. Item has associated value which is evaluated. * Use [set] to change item to this state. * * Config is cascading. * Config can fork from another config by adding a new layer on it. * The forked config is called child config, and the original config is called parent config. * A config without parent config is called root config. The new layer added by child config * is called facade layer. * Config with ancestor configs has multiple layers. All set operation is executed in facade layer * of config. * Descendant config inherits items and values in ancestor configs, and can override values for * items in ancestor configs. Overridden values in config will affect itself and its descendant * configs, without affecting its ancestor configs. Loading items in config will not affect its * ancestor configs too. [invoke] can be used to create a root config, and [withLayer] can be used * to create a child config from specified config. * * All methods in Config is thread-safe. */ interface Config : ItemContainer { /** * Associate item with specified value without type checking. * * @param item config item * @param value associated value */ fun rawSet(item: Item<*>, value: Any?) /** * Associate item with specified value. * * @param item config item * @param value associated value */ operator fun set(item: Item, value: T) /** * Find item with specified name, and associate it with specified value. * * @param name item name * @param value associated value */ operator fun set(name: String, value: T) /** * Associate item with specified thunk, which can be used to evaluate value for the item. * * @param item config item * @param thunk thunk used to evaluate value for the item */ fun lazySet(item: Item, thunk: (config: ItemContainer) -> T) /** * Find item with specified name, and associate item with specified thunk, * which can be used to evaluate value for the item. * * @param name item name * @param thunk thunk used to evaluate value for the item */ fun lazySet(name: String, thunk: (config: ItemContainer) -> T) /** * Discard associated value of specified item. * * @param item config item */ fun unset(item: Item<*>) /** * Discard associated value of item with specified name. * * @param name item name */ fun unset(name: String) /** * Subscribe the update event before every set operation. * * @param beforeSetFunction the subscription function * @return the handler to cancel this subscription */ fun beforeSet(beforeSetFunction: (item: Item<*>, value: Any?) -> Unit): Handler /** * Subscribe the update event after every set operation. * * @param afterSetFunction the subscription function * @return the handler to cancel this subscription */ fun afterSet(afterSetFunction: (item: Item<*>, value: Any?) -> Unit): Handler /** * Remove all values from the facade layer of this config. */ fun clear() /** * Remove all values from all layers of this config. */ fun clearAll() /** * Whether all required items have values or not. * * @return `true` if all required items have values, `false` otherwise */ fun containsRequired(): Boolean /** * Validate whether all required items have values or not. If not, throws [UnsetValueException]. * * @return the current config */ fun validateRequired(): Config /** * Returns a property that can read/set associated value for specified item. * * @param item config item * @return a property that can read/set associated value for specified item */ fun property(item: Item): ReadWriteProperty /** * Returns a property that can read/set associated value for item with specified name. * * @param name item name * @return a property that can read/set associated value for item with specified name */ fun property(name: String): ReadWriteProperty /** * Name of facade layer of config. * * Layer name provides information for facade layer in a cascading config. */ val name: String /** * Returns parent of this config, or `null` if this config is a root config. */ val parent: Config? /** * List of config specs from all layers of this config. */ val specs: List /** * List of sources from all layers of this config. */ val sources: List /** * Returns a config overlapped by the specified facade config. * * All operations will be applied to the facade config first, * and then fall back to this config when necessary. * * @param config the facade config * @return a config overlapped by the specified facade config */ operator fun plus(config: Config): Config /** * Returns a config backing by the specified fallback config. * * All operations will be applied to this config first, * and then fall back to the fallback config when necessary. * * @param config the fallback config * @return a config backing by the specified fallback config */ fun withFallback(config: Config): Config /** * Returns sub-config in the specified path. * * @param path the specified path * @return sub-config in the specified path */ fun at(path: String): Config /** * Returns config with the specified additional prefix. * * @param prefix additional prefix * @return config with the specified additional prefix */ fun withPrefix(prefix: String): Config /** * Load item into facade layer with the specified prefix. * * Same item cannot be added twice. * The item cannot have same qualified name with existed items in config. * * @param item config item * @param prefix prefix for the config item */ fun addItem(item: Item<*>, prefix: String = "") /** * Load items in specified config spec into facade layer. * * Same config spec cannot be added twice. * All items in specified config spec cannot have same qualified name with existed items in config. * * @param spec config spec */ fun addSpec(spec: Spec) /** * Executes the given [action] after locking the facade layer of this config. * * @param action the given action * @return the return value of the action. */ fun lock(action: () -> T): T /** * Returns a child config of this config with specified name. * * @param name name of facade layer in child config * @return a child config */ fun withLayer(name: String = ""): Config /** * Returns a child config containing values from specified source. * * Values from specified source will be loaded into facade layer of the returned child config * without affecting this config. * * @param source config source * @return a child config containing value from specified source */ fun withSource(source: Source): Config /** * Returns a child config containing values loaded by specified trigger. * * Values loaded by specified trigger will be loaded into facade layer of * the returned child config without affecting this config. * * @param description trigger description * @param trigger load trigger * @return a child config containing value loaded by specified trigger */ fun withLoadTrigger( description: String, trigger: ( config: Config, load: (source: Source) -> Unit ) -> Unit ): Config /** * Subscribe the update event before every load operation. * * @param beforeLoadFunction the subscription function * @return the handler to cancel this subscription */ fun beforeLoad(beforeLoadFunction: (source: Source) -> Unit): Handler /** * Subscribe the update event after every load operation. * * @param afterLoadFunction the subscription function * @return the handler to cancel this subscription */ fun afterLoad(afterLoadFunction: (source: Source) -> Unit): Handler /** * Returns default loaders for this config. * * It is a fluent API for loading source from default loaders. * * @return default loaders for this config */ @JavaApi fun from(): DefaultLoaders = from /** * Returns default loaders for this config. * * It is a fluent API for loading source from default loaders. */ val from: DefaultLoaders get() = DefaultLoaders(this) /** * Returns [ObjectMapper] using to map from source to value in config. */ val mapper: ObjectMapper /** * Returns a map in key-value format for this config. * * The returned map contains all items in this config, with item name as key and * associated value as value. * This map can be loaded into config as [com.uchuhimo.konf.source.base.KVSource] using * `config.from.map.kv(map)`. */ fun toMap(): Map /** * Enables the specified feature and returns this config. * * @param feature the specified feature * @return this config */ fun enable(feature: Feature): Config /** * Disables the specified feature and returns this config. * * @param feature the specified feature * @return this config */ fun disable(feature: Feature): Config /** * Check whether the specified feature is enabled or not. * * @param feature the specified feature * @return whether the specified feature is enabled or not */ fun isEnabled(feature: Feature): Boolean companion object { /** * Create a new root config. * * @return a new root config */ operator fun invoke(): Config = BaseConfig() /** * Create a new root config and initiate it. * * @param init initial action * @return a new root config */ operator fun invoke(init: Config.() -> Unit): Config = Config().apply(init) } } /** * Returns a property that can read/set associated value casted from config. * * @return a property that can read/set associated value casted from config */ inline fun Config.cast() = object : RequiredConfigProperty(this.withPrefix("root").withLayer(), name = "root") {} /** * Returns a value casted from config. * * @return a value casted from config */ inline fun Config.toValue(): T { val value by cast() return value } /** * Returns a property that can read/set associated value for specified required item. * * @param prefix prefix for the config item * @param name item name without prefix * @param description description for this item * @return a property that can read/set associated value for specified required item */ inline fun Config.required( prefix: String = "", name: String? = null, description: String = "" ) = object : RequiredConfigProperty(this, prefix, name, description, null is T) {} open class RequiredConfigProperty( private val config: Config, private val prefix: String = "", private val name: String? = null, private val description: String = "", private val nullable: Boolean = false ) : PropertyDelegateProvider> { override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadWriteProperty { val type: JavaType = TypeFactory.defaultInstance().constructType(this::class.java) .findSuperType(RequiredConfigProperty::class.java).bindings.typeParameters[0] val item = object : RequiredItem( Spec.dummy, name ?: property.name, description, type, nullable ) {} config.addItem(item, prefix) return config.property(item) } } /** * Returns a property that can read/set associated value for specified optional item. * * @param default default value returned before associating this item with specified value * @param prefix prefix for the config item * @param name item name without prefix * @param description description for this item * @return a property that can read/set associated value for specified optional item */ inline fun Config.optional( default: T, prefix: String = "", name: String? = null, description: String = "" ) = object : OptionalConfigProperty(this, default, prefix, name, description, null is T) {} open class OptionalConfigProperty( private val config: Config, private val default: T, private val prefix: String = "", private val name: String? = null, private val description: String = "", private val nullable: Boolean = false ) : PropertyDelegateProvider> { override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadWriteProperty { val type: JavaType = TypeFactory.defaultInstance().constructType(this::class.java) .findSuperType(OptionalConfigProperty::class.java).bindings.typeParameters[0] val item = object : OptionalItem( Spec.dummy, name ?: property.name, default, description, type, nullable ) {} config.addItem(item, prefix) return config.property(item) } } /** * Returns a property that can read/set associated value for specified lazy item. * * @param prefix prefix for the config item * @param name item name without prefix * @param description description for this item * @param thunk thunk used to evaluate value for this item * @return a property that can read/set associated value for specified lazy item */ inline fun Config.lazy( prefix: String = "", name: String? = null, description: String = "", noinline thunk: (config: ItemContainer) -> T ) = object : LazyConfigProperty(this, thunk, prefix, name, description, null is T) {} open class LazyConfigProperty( private val config: Config, private val thunk: (config: ItemContainer) -> T, private val prefix: String = "", private val name: String? = null, private val description: String = "", private val nullable: Boolean = false ) : PropertyDelegateProvider> { override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadWriteProperty { val type: JavaType = TypeFactory.defaultInstance().constructType(this::class.java) .findSuperType(LazyConfigProperty::class.java).bindings.typeParameters[0] val item = object : LazyItem( Spec.dummy, name ?: property.name, thunk, description, type, nullable ) {} config.addItem(item, prefix) return config.property(item) } } /** * Convert the config to a tree node. * * @return a tree node */ fun Config.toTree(): TreeNode { return toMap().kvToTree() } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/ConfigException.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf /** * Exception for config. */ open class ConfigException : RuntimeException { constructor(message: String) : super(message) constructor(message: String, cause: Throwable) : super(message, cause) } /** * Exception indicates that there is existed item with same name in config. */ class RepeatedItemException(val name: String) : ConfigException("item $name has been added") /** * Exception indicates that there is existed inner spec in config. */ class RepeatedInnerSpecException(val spec: Spec) : ConfigException("spec ${spec.javaClass.simpleName}(prefix=\"${spec.prefix}\") has been added") /** * Exception indicates that there is existed item with conflicted name in config. */ class NameConflictException(message: String) : ConfigException(message) /** * Exception indicates that the evaluated result of lazy thunk is invalid. */ class InvalidLazySetException(message: String) : ConfigException(message) val Item<*>.asName: String get() = "item $name" /** * Exception indicates that the specified item is in unset state. */ class UnsetValueException(val name: String) : ConfigException("$name is unset") { constructor(item: Item<*>) : this(item.asName) } /** * Exception indicates that the specified item has default value. */ class GetDefaultValueException(val name: String) : ConfigException("$name has default value") { constructor(item: Item<*>) : this(item.asName) } /** * Exception indicates that the specified item is not in this config. */ class NoSuchItemException(val name: String) : ConfigException("cannot find $name in config") { constructor(item: Item<*>) : this(item.asName) } /** * Exception indicates that item cannot be added to this config because it has child layer. */ class LayerFrozenException(val config: Config) : ConfigException("config ${config.name} has child layer, cannot add new item") /** * Exception indicates that expected value in specified path is not existed in the source. */ class NoSuchPathException(val path: String) : ConfigException("cannot find path \"$path\" in config spec") /** * Exception indicates that the specified path is invalid. */ class InvalidPathException(val path: String) : ConfigException("\"$path\" is not a valid path") /** * Exception indicates that the specified path conflicts with existed paths in the tree node. */ class PathConflictException(val path: String) : ConfigException("\"$path\" conflicts with existed paths in the tree node") ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/ConfigSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.fasterxml.jackson.module.kotlin.isKotlinClass /** * The default implementation for [Spec]. * * @param prefix common prefix for items in this config spec */ open class ConfigSpec @JvmOverloads constructor( prefix: String? = null, items: Set> = mutableSetOf(), innerSpecs: Set = mutableSetOf() ) : Spec { final override val prefix: String = prefix ?: { if (javaClass == ConfigSpec::class.java || javaClass.isAnonymousClass) { "" } else { javaClass.let { clazz -> if (this::class.isCompanion) clazz.declaringClass else clazz }.simpleName.let { name -> if (name == null || name.contains('$')) { "" } else { name.toLittleCase() } }.let { name -> if (name.endsWith("Spec")) { name.removeSuffix("Spec") } else { name } } } }() init { checkPath(this.prefix) } private val _items = items as? MutableSet> ?: items.toMutableSet() override val items: Set> = _items override fun addItem(item: Item<*>) { if (item !in _items) { _items += item } else { throw RepeatedItemException(item.name) } } private val _innerSpecs = innerSpecs as? MutableSet ?: innerSpecs.toMutableSet() override val innerSpecs: Set = _innerSpecs override fun addInnerSpec(spec: Spec) { if (spec !in _innerSpecs) { _innerSpecs += spec } else { throw RepeatedInnerSpecException(spec) } } init { if (javaClass.isKotlinClass()) { javaClass.kotlin.nestedClasses.map { it.objectInstance }.filterIsInstance().forEach { spec -> addInnerSpec(spec) } } } /** * Specify a required item in this config spec. * * @param name item name without prefix * @param description description for this item * @return a property of a required item with prefix of this config spec */ inline fun required(name: String? = null, description: String = "") = object : RequiredProperty(this, name, description, null is T) {} /** * Specify an optional item in this config spec. * * @param default default value returned before associating this item with specified value * @param name item name without prefix * @param description description for this item * * @return a property of an optional item with prefix of this config spec */ inline fun optional(default: T, name: String? = null, description: String = "") = object : OptionalProperty(this, default, name, description, null is T) {} /** * Specify a lazy item in this config spec. * * @param name item name without prefix * @param description description for this item * @param thunk thunk used to evaluate value for this item * @return a property of a lazy item with prefix of this config spec */ inline fun lazy( name: String? = null, description: String = "", noinline thunk: (config: ItemContainer) -> T ) = object : LazyProperty(this, thunk, name, description, null is T) {} } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/Feature.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf /** * Enumeration that defines simple on/off features. */ enum class Feature(val enabledByDefault: Boolean) { /** * Feature that determines what happens when unknown paths appear in the source. * If enabled, an exception is thrown when loading from the source * to indicate it contains unknown paths. * * Feature is disabled by default. */ FAIL_ON_UNKNOWN_PATH(false), /** * Feature that determines whether loading keys from sources case-insensitively. * * Feature is disabled by default. */ LOAD_KEYS_CASE_INSENSITIVELY(false), /** * Feature that determines whether loading keys from sources as little camel case. * * Feature is enabled by default. */ LOAD_KEYS_AS_LITTLE_CAMEL_CASE(true), /** * Feature that determines whether sources are optional by default. * * Feature is disabled by default. */ OPTIONAL_SOURCE_BY_DEFAULT(false), /** * Feature that determines whether sources should be substituted before loaded into config. * * Feature is enabled by default. */ SUBSTITUTE_SOURCE_BEFORE_LOADED(true) } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/Item.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.type.TypeFactory /** * Item that can be contained by config. * * Item can be associated with value in config, containing metadata for the value. * The metadata for value includes name, path, type, description and so on. * Item can be used as key to operate value in config, guaranteeing type safety. * There are three kinds of item: [required item][RequiredItem], [optional item][OptionalItem] * and [lazy item][LazyItem]. * * @param T type of value that can be associated with this item. * @param spec config spec that contains this item * @param name item name without prefix * @param description description for this item * @see Config */ sealed class Item( /** * Config spec that contains this item. */ val spec: Spec, /** * Item name without prefix. */ val name: String, /** * Description for this item. */ val description: String = "", type: JavaType? = null, val nullable: Boolean = false ) { init { checkPath(name) @Suppress("LeakingThis") spec.addItem(this) } /** * Item path without prefix. */ val path: Path = this.name.toPath() /** * Type of value that can be associated with this item. */ @Suppress("LeakingThis") val type: JavaType = type ?: TypeFactory.defaultInstance().constructType(this::class.java) .findSuperType(Item::class.java).bindings.typeParameters[0] /** * Whether this is a required item or not. */ open val isRequired: Boolean get() = false /** * Whether this is an optional item or not. */ open val isOptional: Boolean get() = false /** * Whether this is a lazy item or not. */ open val isLazy: Boolean get() = false /** * Cast this item to a required item. */ val asRequiredItem: RequiredItem get() = this as RequiredItem /** * Cast this item to an optional item. */ val asOptionalItem: OptionalItem get() = this as OptionalItem /** * Cast this item to a lazy item. */ val asLazyItem: LazyItem get() = this as LazyItem private val onSetFunctions: MutableList<(value: T) -> Unit> = mutableListOf() private val beforeSetFunctions: MutableList<(config: Config, value: T) -> Unit> = mutableListOf() private val afterSetFunctions: MutableList<(config: Config, value: T) -> Unit> = mutableListOf() /** * Subscribe the update event of this item. * * @param onSetFunction the subscription function * @return the handler to cancel this subscription */ fun onSet(onSetFunction: (value: T) -> Unit): Handler { onSetFunctions += onSetFunction return object : Handler { override fun cancel() { onSetFunctions.remove(onSetFunction) } } } /** * Subscribe the update event of this item before every set operation. * * @param beforeSetFunction the subscription function * @return the handler to cancel this subscription */ fun beforeSet(beforeSetFunction: (config: Config, value: T) -> Unit): Handler { beforeSetFunctions += beforeSetFunction return object : Handler { override fun cancel() { beforeSetFunctions.remove(beforeSetFunction) } } } /** * Subscribe the update event of this item after every set operation. * * @param afterSetFunction the subscription function * @return the handler to cancel this subscription */ fun afterSet(afterSetFunction: (config: Config, value: T) -> Unit): Handler { afterSetFunctions += afterSetFunction return object : Handler { override fun cancel() { afterSetFunctions.remove(afterSetFunction) } } } fun notifySet(value: Any?) { for (onSetFunction in onSetFunctions) { @Suppress("UNCHECKED_CAST") onSetFunction(value as T) } } fun notifyBeforeSet(config: Config, value: Any?) { for (beforeSetFunction in beforeSetFunctions) { @Suppress("UNCHECKED_CAST") beforeSetFunction(config, value as T) } } fun notifyAfterSet(config: Config, value: Any?) { for (afterSetFunction in afterSetFunctions) { @Suppress("UNCHECKED_CAST") afterSetFunction(config, value as T) } } } interface Handler : AutoCloseable { fun cancel() override fun close() { cancel() } } /** * Type of Item path. */ typealias Path = List /** * Returns corresponding item name of the item path. * * @receiver item path * @return item name */ val Path.name: String get() = joinToString(".") /** * Returns corresponding item path of the item name. * * @receiver item name * @return item path */ fun String.toPath(): Path { val name = this.trim() return if (name.isEmpty()) { listOf() } else { val path = name.split('.') if ("" in path) { throw InvalidPathException(this) } path } } fun checkPath(path: String) { val trimmedPath = path.trim() if (trimmedPath.isNotEmpty()) { if ("" in trimmedPath.split('.')) { throw InvalidPathException(path) } } } /** * Required item without default value. * * Required item must be set with value before retrieved in config. */ open class RequiredItem @JvmOverloads constructor( spec: Spec, name: String, description: String = "", type: JavaType? = null, nullable: Boolean = false ) : Item(spec, name, description, type, nullable) { override val isRequired: Boolean = true } /** * Optional item with default value. * * Before associated with specified value, default value will be returned when accessing. * After associated with specified value, the specified value will be returned when accessing. */ open class OptionalItem @JvmOverloads constructor( spec: Spec, name: String, /** * Default value returned before associating this item with specified value. */ val default: T, description: String = "", type: JavaType? = null, nullable: Boolean = false ) : Item(spec, name, description, type, nullable) { init { if (!nullable) { requireNotNull(default) } } override val isOptional: Boolean = true } /** * Lazy item evaluated value every time from thunk before associated with specified value. * * Before associated with specified value, value evaluated from thunk will be returned when accessing. * After associated with specified value, the specified value will be returned when accessing. * Returned value of the thunk will not be cached. The thunk will be evaluated every time * when needed to reflect modifying of other values in config. */ open class LazyItem @JvmOverloads constructor( spec: Spec, name: String, /** * Thunk used to evaluate value for this item. * * [ItemContainer] is provided as evaluation environment to avoid unexpected modification * to config. * Thunk will be evaluated every time when needed to reflect modifying of other values in config. */ val thunk: (config: ItemContainer) -> T, description: String = "", type: JavaType? = null, nullable: Boolean = false ) : Item(spec, name, description, type, nullable) { override val isLazy: Boolean = true } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/ItemContainer.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf /** * Container of items. * * Item container contains read-only access operations for item. * * @see Config */ interface ItemContainer : Iterable> { /** * Get associated value with specified item. * * @param item config item * @return associated value */ operator fun get(item: Item): T /** * Get associated value with specified item name. * * @param name item name * @return associated value */ operator fun get(name: String): T /** * Returns associated value if specified item exists, `null` otherwise. * * @param item config item * @return associated value if specified item exists, `null` otherwise */ fun getOrNull(item: Item): T? /** * Returns associated value if specified item name exists, `null` otherwise. * * @param name item name * @return associated value if specified item name exists, `null` otherwise */ fun getOrNull(name: String): T? /** * Get associated value with specified item name. * * @param name item name * @return associated value */ operator fun invoke(name: String): T = get(name) /** * Returns iterator of items in this item container. * * @return iterator of items in this item container */ override operator fun iterator(): Iterator> /** * Whether this item container contains specified item or not. * * @param item config item * @return `true` if this item container contains specified item, `false` otherwise */ operator fun contains(item: Item<*>): Boolean /** * Whether this item container contains item with specified name or not. * * @param name item name * @return `true` if this item container contains item with specified name, `false` otherwise */ operator fun contains(name: String): Boolean /** * Whether this item container contains the specified path or not. * * @param path the specified path * @return `true` if this item container contains the specified path, `false` otherwise */ operator fun contains(path: Path): Boolean /** * Returns the qualified name of the specified item. * * @param item the specified item * @return the qualified name of the specified item */ fun nameOf(item: Item<*>): String /** * Returns the qualified path of the specified item. * * @param item the specified item * @return the qualified path of the specified item */ fun pathOf(item: Item<*>): Path = nameOf(item).toPath() /** * List of items in this item container. */ val items: List> get() = mutableListOf>().apply { addAll(this@ItemContainer.iterator().asSequence()) } /** * List of qualified names of items in this item container. */ val nameOfItems: List get() = itemWithNames.map { it.second } /** * List of items with the corresponding qualified names in this item container. */ val itemWithNames: List, String>> } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/MergedConfig.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.uchuhimo.konf.source.Source /** * Config that merge [fallback] and [facade]. * * All operations will be applied to [facade] first, and then fall back to [facade] when necessary. */ open class MergedConfig(val fallback: BaseConfig, val facade: BaseConfig) : BaseConfig("merged(facade=${facade.name.notEmptyOr("\"\"")}, fallback=${fallback.name.notEmptyOr("\"\"")})") { override fun rawSet(item: Item<*>, value: Any?) { if (item in facade) { facade.rawSet(item, value) } else { fallback.rawSet(item, value) } } override fun getItemOrNull(name: String): Item<*>? { return facade.getItemOrNull(name) ?: fallback.getItemOrNull(name) } override fun lazySet(item: Item, thunk: (config: ItemContainer) -> T) { if (item in facade) { facade.lazySet(item, thunk) } else { fallback.lazySet(item, thunk) } } override fun unset(item: Item<*>) { if (item in facade) { facade.unset(item) } else { fallback.unset(item) } } override fun addBeforeLoadFunction(beforeLoadFunction: (source: Source) -> Unit) { facade.addBeforeLoadFunction(beforeLoadFunction) fallback.addBeforeLoadFunction(beforeLoadFunction) } override fun removeBeforeLoadFunction(beforeLoadFunction: (source: Source) -> Unit) { facade.removeBeforeLoadFunction(beforeLoadFunction) fallback.removeBeforeLoadFunction(beforeLoadFunction) } override fun addAfterLoadFunction(afterLoadFunction: (source: Source) -> Unit) { facade.addAfterLoadFunction(afterLoadFunction) fallback.addAfterLoadFunction(afterLoadFunction) } override fun removeAfterLoadFunction(afterLoadFunction: (source: Source) -> Unit) { facade.removeAfterLoadFunction(afterLoadFunction) fallback.removeAfterLoadFunction(afterLoadFunction) } override fun addBeforeSetFunction(beforeSetFunction: (item: Item<*>, value: Any?) -> Unit) { facade.addBeforeSetFunction(beforeSetFunction) fallback.addBeforeSetFunction(beforeSetFunction) } override fun removeBeforeSetFunction(beforeSetFunction: (item: Item<*>, value: Any?) -> Unit) { facade.removeBeforeSetFunction(beforeSetFunction) fallback.removeBeforeSetFunction(beforeSetFunction) } override fun addAfterSetFunction(afterSetFunction: (item: Item<*>, value: Any?) -> Unit) { facade.addAfterSetFunction(afterSetFunction) fallback.addAfterSetFunction(afterSetFunction) } override fun removeAfterSetFunction(afterSetFunction: (item: Item<*>, value: Any?) -> Unit) { facade.removeAfterSetFunction(afterSetFunction) fallback.removeAfterSetFunction(afterSetFunction) } override fun clear() { facade.clear() fallback.clear() } override fun clearAll() { facade.clearAll() fallback.clearAll() } override val specs: List get() = facade.specs + fallback.specs override val sources: List get() = facade.sources.toMutableList().apply { for (source in fallback.sources) { add(source) } } override fun addItem(item: Item<*>, prefix: String) { val path = prefix.toPath() + item.name.toPath() val name = path.name if (item !in fallback) { if (path in fallback) { throw NameConflictException("item $name cannot be added") } } else { throw RepeatedItemException(name) } facade.addItem(item, prefix) } override fun addSpec(spec: Spec) { spec.items.forEach { item -> val name = spec.qualify(item) if (item !in fallback) { val path = name.toPath() if (path in fallback) { throw NameConflictException("item $name cannot be added") } } else { throw RepeatedItemException(name) } } facade.addSpec(spec) } override fun lock(action: () -> T): T = facade.lock { fallback.lock(action) } override fun getOrNull( item: Item<*>, errorWhenNotFound: Boolean, errorWhenGetDefault: Boolean, lazyContext: ItemContainer ): Any? { if (item in facade && item in fallback) { try { return facade.getOrNull(item, errorWhenNotFound, true, lazyContext) } catch (ex: Exception) { when (ex) { is UnsetValueException -> { return fallback.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) } is GetDefaultValueException -> { try { return fallback.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) } catch (ex: Exception) { when (ex) { is UnsetValueException -> { if (errorWhenGetDefault) { throw GetDefaultValueException(item) } else { return (item as OptionalItem).default } } else -> throw ex } } } else -> throw ex } } } else if (item in facade) { return facade.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) } else { return fallback.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) } } override fun iterator(): Iterator> = (facade.iterator().asSequence() + fallback.iterator().asSequence()).iterator() override fun contains(item: Item<*>): Boolean = item in facade || item in fallback override fun contains(name: String): Boolean = name in facade || name in fallback override fun contains(path: Path): Boolean = path in facade || path in fallback override fun nameOf(item: Item<*>): String { return if (item in facade) { facade.nameOf(item) } else { fallback.nameOf(item) } } override val itemWithNames: List, String>> get() = facade.itemWithNames + fallback.itemWithNames } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/MergedMap.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf class MergedMap(val fallback: MutableMap, val facade: MutableMap) : MutableMap { override val size: Int get() = keys.size override fun containsKey(key: K): Boolean { return facade.containsKey(key) || fallback.containsKey(key) } override fun containsValue(value: V): Boolean { return facade.containsValue(value) || fallback.containsValue(value) } override fun get(key: K): V? { return facade[key] ?: fallback[key] } override fun isEmpty(): Boolean { return facade.isEmpty() && fallback.isEmpty() } override val entries: MutableSet> get() = keys.map { it to getValue(it) }.toMap(LinkedHashMap()).entries override val keys: MutableSet get() = facade.keys.union(fallback.keys).toMutableSet() override val values: MutableCollection get() = keys.map { getValue(it) }.toMutableList() override fun clear() { facade.clear() fallback.clear() } override fun put(key: K, value: V): V? { return facade.put(key, value) } override fun putAll(from: Map) { facade.putAll(from) } override fun remove(key: K): V? { if (key in facade) { if (key in fallback) { fallback.remove(key) } return facade.remove(key) } else { return fallback.remove(key) } } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/Prefix.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.uchuhimo.konf.source.Source /** * Convenient class for constructing [Spec]/[Config]/[Source] with prefix. */ data class Prefix( /** * The path of the prefix */ val path: String = "" ) { /** * Returns a config spec with this prefix. * * @param spec the base config spec * @return a config spec with this prefix */ operator fun plus(spec: Spec): Spec = spec.withPrefix(path) /** * Returns a config with this prefix. * * @param config the base config * @return a config with this prefix */ operator fun plus(config: Config): Config = config.withPrefix(path) /** * Returns a source with this prefix. * * @param source the base source * @return a source with this prefix */ operator fun plus(source: Source): Source = source.withPrefix(path) } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/SizeInBytes.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonValue import com.uchuhimo.konf.source.ParseException import java.io.Serializable import java.math.BigDecimal import java.math.BigInteger /** * Represents size in unit of bytes. */ data class SizeInBytes( /** * Number of bytes. */ @JsonValue val bytes: Long ) : Serializable { init { require(bytes >= 0) } companion object { /** * Parses a size-in-bytes string. If no units are specified in the string, * it is assumed to be in bytes. The returned value is in bytes. * * @param input the string to parse * @return size in bytes */ @JsonCreator @JvmStatic fun parse(input: String): SizeInBytes { val s = input.trim() val unitString = getUnits(s) val numberString = s.substring( 0, s.length - unitString.length ).trim() // this would be caught later anyway, but the error message // is more helpful if we check it here. if (numberString.isEmpty()) throw ParseException("No number in size-in-bytes value '$input'") val units = MemoryUnit.parseUnit(unitString) ?: throw ParseException( "Could not parse size-in-bytes unit '$unitString'" + " (try k, K, kB, KiB, kilobytes, kibibytes)" ) try { val result: BigInteger // if the string is purely digits, parse as an integer to avoid // possible precision loss; otherwise as a double. result = if (numberString.matches("[0-9]+".toRegex())) { units.bytes.multiply(BigInteger(numberString)) } else { val resultDecimal = BigDecimal(units.bytes).multiply(BigDecimal(numberString)) resultDecimal.toBigInteger() } return if (result.bitLength() < 64) { SizeInBytes(result.toLong()) } else { throw ParseException("size-in-bytes value is out of range for a 64-bit long: '$input'") } } catch (e: NumberFormatException) { throw ParseException("Could not parse size-in-bytes number '$numberString'") } } private enum class Radix { KILO { override fun toInt(): Int = 1000 }, KIBI { override fun toInt(): Int = 1024 }; abstract fun toInt(): Int } private enum class MemoryUnit( private val prefix: String, private val radix: Radix, private val power: Int ) { BYTES("", Radix.KIBI, 0), KILOBYTES("kilo", Radix.KILO, 1), MEGABYTES("mega", Radix.KILO, 2), GIGABYTES("giga", Radix.KILO, 3), TERABYTES("tera", Radix.KILO, 4), PETABYTES("peta", Radix.KILO, 5), EXABYTES("exa", Radix.KILO, 6), ZETTABYTES("zetta", Radix.KILO, 7), YOTTABYTES("yotta", Radix.KILO, 8), KIBIBYTES("kibi", Radix.KIBI, 1), MEBIBYTES("mebi", Radix.KIBI, 2), GIBIBYTES("gibi", Radix.KIBI, 3), TEBIBYTES("tebi", Radix.KIBI, 4), PEBIBYTES("pebi", Radix.KIBI, 5), EXBIBYTES("exbi", Radix.KIBI, 6), ZEBIBYTES("zebi", Radix.KIBI, 7), YOBIBYTES("yobi", Radix.KIBI, 8); internal val bytes: BigInteger = BigInteger.valueOf(radix.toInt().toLong()).pow(power) companion object { private val unitsMap = mutableMapOf().apply { for (unit in MemoryUnit.values()) { put(unit.prefix + "byte", unit) put(unit.prefix + "bytes", unit) if (unit.prefix.isEmpty()) { put("b", unit) put("B", unit) put("", unit) // no unit specified means bytes } else { val first = unit.prefix.substring(0, 1) val firstUpper = first.toUpperCase() when (unit.radix) { Radix.KILO -> { if (unit.power == 1) { put(first + "B", unit) // 512kB } else { put(firstUpper + "B", unit) // 512MB } } Radix.KIBI -> { put(first, unit) // 512m put(firstUpper, unit) // 512M put(firstUpper + "i", unit) // 512Mi put(firstUpper + "iB", unit) // 512MiB } } } } } internal fun parseUnit(unit: String): MemoryUnit? { return unitsMap[unit] } } } } } /** * Converts a string to [SizeInBytes]. */ fun String.toSizeInBytes(): SizeInBytes = SizeInBytes.parse(this) ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/Spec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.type.TypeFactory import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty /** * Config spec is specification for config. * * Config spec describes a group of items with common prefix, which can be loaded into config * together using [Config.addSpec]. * Config spec also provides convenient API to specify item in it without hand-written object * declaration. * * @see Config */ interface Spec { /** * Common prefix for items in this config spec. * * An empty prefix means names of items in this config spec are unqualified. */ val prefix: String /** * Qualify item name with prefix of this config spec. * * When prefix is empty, original item name will be returned. * * @param item the config item * @return qualified item name */ fun qualify(item: Item<*>): String = (prefix.toPath() + item.path).name /** * Add the specified item into this config spec. * * @param item the specified item */ fun addItem(item: Item<*>) /** * Set of specified items in this config spec. */ val items: Set> /** * Add the specified inner spec into this config spec. * * @param spec the specified spec */ fun addInnerSpec(spec: Spec) /** * Set of inner specs in this config spec. */ val innerSpecs: Set /** * Returns a config spec overlapped by the specified facade config spec. * * New items will be added to the facade config spec. * * @param spec the facade config spec * @return a config spec overlapped by the specified facade config spec */ operator fun plus(spec: Spec): Spec { return object : Spec by spec { override fun addItem(item: Item<*>) { if (item !in this@Spec.items) { spec.addItem(item) } else { throw RepeatedItemException(item.name) } } override val items: Set> get() = this@Spec.items + spec.items override fun qualify(item: Item<*>): String { return if (item in spec.items) { spec.qualify(item) } else { this@Spec.qualify(item) } } } } /** * Returns a config spec backing by the specified fallback config spec. * * New items will be added to the current config spec. * * @param spec the fallback config spec * @return a config spec backing by the specified fallback config spec */ fun withFallback(spec: Spec): Spec = spec + this /** * Returns sub-spec in the specified path. * * @param path the specified path * @return sub-source with specified prefix */ operator fun get(path: String): Spec = get(prefix.toPath(), path.toPath()) private fun get(prefix: Path, path: Path): Spec { return if (path.isEmpty()) { this } else if (prefix.size >= path.size && prefix.subList(0, path.size) == path) { ConfigSpec(prefix.subList(path.size, prefix.size).name, items, innerSpecs) } else { if (prefix.size < path.size && path.subList(0, prefix.size) == prefix) { val pathForInnerSpec = path.subList(prefix.size, path.size).name val filteredInnerSpecs = innerSpecs.mapNotNull { spec -> try { spec[pathForInnerSpec] } catch (_: NoSuchPathException) { null } } if (filteredInnerSpecs.isEmpty()) { throw NoSuchPathException(path.name) } else if (filteredInnerSpecs.size == 1) { return filteredInnerSpecs[0] } else { ConfigSpec("", emptySet(), filteredInnerSpecs.toMutableSet()) } } else { throw NoSuchPathException(path.name) } } } /** * Returns config spec with the specified additional prefix. * * @param prefix additional prefix * @return config spec with the specified additional prefix */ fun withPrefix(prefix: String): Spec = withPrefix(this.prefix.toPath(), prefix.toPath()) private fun withPrefix(prefix: Path, newPrefix: Path): Spec { return if (newPrefix.isEmpty()) { this } else { ConfigSpec((newPrefix + prefix).name, items, innerSpecs) } } companion object { /** * A dummy implementation for [Spec]. * * It will swallow all items added to it. Used for items belonged to no config spec. */ val dummy: Spec = object : Spec { override val prefix: String = "" override fun addItem(item: Item<*>) {} override val items: Set> = emptySet() override fun addInnerSpec(spec: Spec) {} override val innerSpecs: Set = emptySet() } } } /** * Specify a required item in this config spec. * * @param name item name without prefix * @param description description for this item * @return a property of a required item with prefix of this config spec */ inline fun Spec.required(name: String? = null, description: String = "") = object : RequiredProperty(this, name, description, null is T) {} open class RequiredProperty( private val spec: Spec, private val name: String? = null, private val description: String = "", private val nullable: Boolean = false ) : PropertyDelegateProvider>> { override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> { val type: JavaType = TypeFactory.defaultInstance().constructType(this::class.java) .findSuperType(RequiredProperty::class.java).bindings.typeParameters[0] val item = object : RequiredItem( spec, name ?: property.name, description, type, nullable ) {} return ReadOnlyProperty> { _, _ -> item } } } /** * Specify an optional item in this config spec. * * @param default default value returned before associating this item with specified value * @param name item name without prefix * @param description description for this item * * @return a property of an optional item with prefix of this config spec */ inline fun Spec.optional(default: T, name: String? = null, description: String = "") = object : OptionalProperty(this, default, name, description, null is T) {} open class OptionalProperty( private val spec: Spec, private val default: T, private val name: String? = null, private val description: String = "", private val nullable: Boolean = false ) : PropertyDelegateProvider>> { override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> { val type: JavaType = TypeFactory.defaultInstance().constructType(this::class.java) .findSuperType(OptionalProperty::class.java).bindings.typeParameters[0] val item = object : OptionalItem( spec, name ?: property.name, default, description, type, nullable ) {} return ReadOnlyProperty> { _, _ -> item } } } /** * Specify a lazy item in this config spec. * * @param name item name without prefix * @param description description for this item * @param thunk thunk used to evaluate value for this item * @return a property of a lazy item with prefix of this config spec */ inline fun Spec.lazy( name: String? = null, description: String = "", noinline thunk: (config: ItemContainer) -> T ) = object : LazyProperty(this, thunk, name, description, null is T) {} open class LazyProperty( private val spec: Spec, private val thunk: (config: ItemContainer) -> T, private val name: String? = null, private val description: String = "", private val nullable: Boolean = false ) : PropertyDelegateProvider>> { override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> { val type: JavaType = TypeFactory.defaultInstance().constructType(this::class.java) .findSuperType(LazyProperty::class.java).bindings.typeParameters[0] val item = object : LazyItem( spec, name ?: property.name, thunk, description, type, nullable ) {} return ReadOnlyProperty> { _, _ -> item } } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import java.util.Collections /** * Tree node that represents internal structure of config/source. */ interface TreeNode { /** * Children nodes in this tree node with their names as keys. */ val children: MutableMap /** * Associate path with specified node. * * @param path path * @param node associated node */ operator fun set(path: Path, node: TreeNode) { if (path.isEmpty()) { throw PathConflictException(path.name) } val key = path.first() if (this is LeafNode) { throw PathConflictException(path.name) } try { return if (path.size == 1) { children[key] = node } else { val rest = path.drop(1) var child = children[key] if (child == null) { child = ContainerNode(mutableMapOf()) children[key] = child } child[rest] = node } } catch (_: PathConflictException) { throw PathConflictException(path.name) } finally { if (this is MapNode && isPlaceHolder) { isPlaceHolder = false } } } /** * Associate path with specified node. * * @param path path * @param node associated node */ operator fun set(path: String, node: TreeNode) { set(path.toPath(), node) } /** * Whether this tree node contains node(s) in specified path or not. * * @param path item path * @return `true` if this tree node contains node(s) in specified path, `false` otherwise */ operator fun contains(path: Path): Boolean { return if (path.isEmpty()) { true } else { val key = path.first() val rest = path.drop(1) val result = children[key] if (result != null) { return rest in result } else { return false } } } /** * Returns tree node in specified path if this tree node contains value(s) in specified path, * `null` otherwise. * * @param path item path * @return tree node in specified path if this tree node contains value(s) in specified path, * `null` otherwise */ fun getOrNull(path: Path): TreeNode? { return if (path.isEmpty()) { this } else { val key = path.first() val rest = path.drop(1) val result = children[key] result?.getOrNull(rest) } } /** * Returns tree node in specified path if this tree node contains value(s) in specified path, * `null` otherwise. * * @param path item path * @return tree node in specified path if this tree node contains value(s) in specified path, * `null` otherwise */ fun getOrNull(path: String): TreeNode? = getOrNull(path.toPath()) /** * Returns a node backing by specified fallback node. * * @param fallback fallback node * @return a node backing by specified fallback node */ fun withFallback(fallback: TreeNode): TreeNode { fun traverseTree(facade: TreeNode, fallback: TreeNode, path: Path): TreeNode { if (facade is LeafNode || fallback is LeafNode) { return facade } else { return ContainerNode( facade.children.toMutableMap().also { map -> for ((key, child) in fallback.children) { if (key in facade.children) { map[key] = traverseTree(facade.children.getValue(key), child, path + key) } else { map[key] = child } } } ) } } return traverseTree(this, fallback, "".toPath()) } /** * Returns a node overlapped by the specified facade node. * * @param facade the facade node * @return a node overlapped by the specified facade node */ operator fun plus(facade: TreeNode): TreeNode = facade.withFallback(this) /** * Returns a tree node containing all nodes of the original tree node * except the nodes contained in the given [other] tree node. * * @return a tree node */ operator fun minus(other: TreeNode): TreeNode { fun traverseTree(left: TreeNode, right: TreeNode): TreeNode { if (left is LeafNode) { return EmptyNode } else { if (right is LeafNode) { return EmptyNode } else { val leftKeys = left.children.keys val rightKeys = right.children.keys val diffKeys = leftKeys - rightKeys val sharedKeys = leftKeys.intersect(rightKeys) val children = mutableMapOf() diffKeys.forEach { key -> children[key] = left.children[key]!! } sharedKeys.forEach { key -> val child = traverseTree(left.children[key]!!, right.children[key]!!) if (child != EmptyNode) { children[key] = child } } return if (children.isEmpty()) { EmptyNode } else { ContainerNode(children) } } } } return traverseTree(this, other) } /** * List of all paths in this tree node. */ val paths: List get() { return mutableListOf().also { list -> fun traverseTree(node: TreeNode, path: Path) { if (node is LeafNode) { list.add(path.name) } else { node.children.forEach { (key, child) -> traverseTree(child, path + key) } } } traverseTree(this, "".toPath()) } } fun firstPath(predicate: (TreeNode) -> Boolean): Path? { fun traverseTree(node: TreeNode, path: Path): Path? { if (predicate(node)) { return path } else { node.children.forEach { (key, child) -> val matchPath = traverseTree(child, path + key) if (matchPath != null) { return matchPath } } return null } } return traverseTree(this, "".toPath()) } /** * Map of all leaves indexed by paths in this tree node. */ val leafByPath: Map get() { return mutableMapOf().also { map -> fun traverseTree(node: TreeNode, path: Path) { if (node is LeafNode) { map[path.name] = node } else { node.children.forEach { (key, child) -> traverseTree(child, path + key) } } } traverseTree(this, "".toPath()) } } fun withoutPlaceHolder(): TreeNode { when (this) { is NullNode -> return this is ValueNode -> return this is ListNode -> return this is MapNode -> { val newChildren = children.mapValues { (_, child) -> child.withoutPlaceHolder() } if (newChildren.isNotEmpty() && newChildren.all { (_, child) -> child is MapNode && child.isPlaceHolder }) { return ContainerNode.placeHolder() } else { return withMap(newChildren.filterValues { !(it is MapNode && it.isPlaceHolder) }) } } else -> return this } } fun isEmpty(): Boolean { when (this) { is EmptyNode -> return true is MapNode -> { return children.isEmpty() || children.all { (_, child) -> child.isEmpty() } } else -> return false } } fun isPlaceHolderNode(): Boolean { when (this) { is MapNode -> { if (isPlaceHolder) { return true } else { return children.isNotEmpty() && children.all { (_, child) -> child.isPlaceHolderNode() } } } else -> return false } } } interface LeafNode : TreeNode interface MapNode : TreeNode { fun withMap(map: Map): MapNode = throw NotImplementedError() var isPlaceHolder: Boolean } val emptyMutableMap: MutableMap = Collections.unmodifiableMap(mutableMapOf()) interface ValueNode : LeafNode { val value: Any override val children: MutableMap get() = emptyMutableMap } interface NullNode : LeafNode interface ListNode : LeafNode { val list: List fun withList(list: List): ListNode = throw NotImplementedError() } /** * Tree node that contains children nodes. */ open class ContainerNode( override val children: MutableMap, override var isPlaceHolder: Boolean = false ) : MapNode { override fun withMap(map: Map): MapNode { val isPlaceHolder = map.isEmpty() && this.isPlaceHolder if (map is MutableMap) { return ContainerNode(map, isPlaceHolder) } else { return ContainerNode(map.toMutableMap(), isPlaceHolder) } } companion object { fun empty(): ContainerNode = ContainerNode(mutableMapOf()) fun placeHolder(): ContainerNode = ContainerNode(mutableMapOf(), true) } } /** * Tree node that represents a empty tree. */ object EmptyNode : LeafNode { override val children: MutableMap = emptyMutableMap } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/Utils.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import java.io.File /** * Throws [UnsupportedOperationException]. * * @throws UnsupportedOperationException */ @Suppress("NOTHING_TO_INLINE") inline fun unsupported(): Nothing { throw UnsupportedOperationException() } internal fun getUnits(s: String): String { var i = s.length - 1 while (i >= 0) { val c = s[i] if (!c.isLetter()) break i -= 1 } return s.substring(i + 1) } /** * Returns default value if string is empty, original string otherwise. */ fun String.notEmptyOr(default: String): String = if (isEmpty()) default else this fun String.toLittleCase(): String { return if (this.all { it.isUpperCase() }) { this.toLowerCase() } else { when (val firstLowerCaseIndex = this.indexOfFirst { it.isLowerCase() }) { -1, 0 -> this 1 -> this[0].toLowerCase() + this.drop(1) else -> this.substring(0, firstLowerCaseIndex - 1).toLowerCase() + this.substring(firstLowerCaseIndex - 1) } } } /** * Modified implementation from [org.apache.commons.text.CaseUtils.toCamelCase]. */ fun String.toCamelCase(): String { if (isEmpty()) { return this } val strLen = this.length val newCodePoints = IntArray(strLen) var outOffset = 0 val delimiterSet = setOf(" ".codePointAt(0), "_".codePointAt(0)) var capitalizeNext = Character.isUpperCase(this.codePointAt(0)) var lowercaseNext = false var previousIsUppercase = false var index = 0 while (index < strLen) { val codePoint: Int = this.codePointAt(index) when { delimiterSet.contains(codePoint) -> { capitalizeNext = outOffset != 0 lowercaseNext = false previousIsUppercase = false index += Character.charCount(codePoint) } capitalizeNext -> { val upperCaseCodePoint = Character.toUpperCase(codePoint) newCodePoints[outOffset++] = upperCaseCodePoint index += Character.charCount(upperCaseCodePoint) capitalizeNext = false lowercaseNext = true } lowercaseNext -> { val lowerCaseCodePoint = Character.toLowerCase(codePoint) newCodePoints[outOffset++] = lowerCaseCodePoint index += Character.charCount(lowerCaseCodePoint) if (Character.isLowerCase(codePoint)) { lowercaseNext = false if (previousIsUppercase) { previousIsUppercase = false val previousCodePoint = newCodePoints[outOffset - 2] val upperCaseCodePoint = Character.toUpperCase(previousCodePoint) newCodePoints[outOffset - 2] = upperCaseCodePoint } } else { previousIsUppercase = true } } else -> { newCodePoints[outOffset++] = codePoint index += Character.charCount(codePoint) } } } return if (outOffset != 0) { String(newCodePoints, 0, outOffset) } else this } fun String.toLittleCamelCase(): String { return this.toCamelCase().toLittleCase() } fun tempDirectory( prefix: String = "tmp", suffix: String? = null, directory: File? = null ): File { return createTempDir(prefix, suffix, directory) } fun tempFile(prefix: String = "tmp", suffix: String? = null, directory: File? = null): File { return createTempFile(prefix, suffix, directory) } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/annotation/Annotations.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.annotation /** * Indicates that this API is specially designed to be used in Java. */ @Target( AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS ) @Retention(AnnotationRetention.SOURCE) annotation class JavaApi ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/DefaultLoaders.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.base.FlatSource import com.uchuhimo.konf.source.base.KVSource import com.uchuhimo.konf.source.base.MapSource import com.uchuhimo.konf.source.env.EnvProvider import com.uchuhimo.konf.source.json.JsonProvider import com.uchuhimo.konf.source.properties.PropertiesProvider import kotlinx.coroutines.Dispatchers import java.io.File import java.net.URL import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext /** * Default loaders for config. * * If [transform] is provided, source will be applied the given [transform] function when loaded. * * @param config parent config for loader * @param transform the given transformation function */ class DefaultLoaders( /** * Parent config for loader. */ val config: Config, /** * The given transformation function. */ private val transform: ((Source) -> Source)? = null ) { val optional = config.isEnabled(Feature.OPTIONAL_SOURCE_BY_DEFAULT) fun Provider.orMapped(): Provider = if (transform != null) this.map(transform) else this fun Source.orMapped(): Source = transform?.invoke(this) ?: this /** * Returns default loaders applied the given [transform] function. * * @param transform the given transformation function * @return the default loaders applied the given [transform] function */ fun mapped(transform: (Source) -> Source): DefaultLoaders = DefaultLoaders(config) { transform(it.orMapped()) } /** * Returns default loaders where sources have specified additional prefix. * * @param prefix additional prefix * @return the default loaders where sources have specified additional prefix */ fun prefixed(prefix: String): DefaultLoaders = mapped { it.withPrefix(prefix) } /** * Returns default loaders where sources are scoped in specified path. * * @param path path that is the scope of sources * @return the default loaders where sources are scoped in specified path */ fun scoped(path: String): DefaultLoaders = mapped { it[path] } fun enabled(feature: Feature): DefaultLoaders = mapped { it.enabled(feature) } fun disabled(feature: Feature): DefaultLoaders = mapped { it.disabled(feature) } /** * Loader for JSON source. */ @JvmField val json = Loader(config, JsonProvider.orMapped()) /** * Loader for properties source. */ @JvmField val properties = Loader(config, PropertiesProvider.orMapped()) /** * Loader for map source. */ @JvmField val map = MapLoader(config, transform) /** * Loader for a source from the specified provider. * * @param provider the specified provider * @return a loader for a source from the specified provider */ fun source(provider: Provider) = Loader(config, provider.orMapped()) /** * Returns a child config containing values from system environment. * * @param nested whether to treat "AA_BB_CC" as nested format "AA.BB.CC" or not. True by default. * @return a child config containing values from system environment */ @JvmOverloads fun env(nested: Boolean = true): Config = config.withSource(EnvProvider.env(nested).orMapped()) @JvmOverloads fun envMap(map: Map, nested: Boolean = true): Config = config.withSource(EnvProvider.envMap(map, nested).orMapped()) /** * Returns a child config containing values from system properties. * * @return a child config containing values from system properties */ fun systemProperties(): Config = config.withSource(PropertiesProvider.system().orMapped()) /** * Returns corresponding loader based on extension. * * @param extension the file extension * @param source the source description for error message * @return the corresponding loader based on extension */ fun dispatchExtension(extension: String, source: String = ""): Loader = Loader( config, Provider.of(extension)?.orMapped() ?: throw UnsupportedExtensionException(source) ) /** * Returns a child config containing values from specified file. * * Format of the file is auto-detected from the file extension. * Supported file formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the file extension is unsupported. * * @param file specified file * @param optional whether the source is optional * @return a child config containing values from specified file * @throws UnsupportedExtensionException */ fun file(file: File, optional: Boolean = this.optional): Config = dispatchExtension(file.extension, file.name).file(file, optional) /** * Returns a child config containing values from specified file path. * * Format of the file is auto-detected from the file extension. * Supported file formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the file extension is unsupported. * * @param file specified file path * @param optional whether the source is optional * @return a child config containing values from specified file path * @throws UnsupportedExtensionException */ fun file(file: String, optional: Boolean = this.optional): Config = file(File(file), optional) /** * Returns a child config containing values from specified file, * and reloads values when file content has been changed. * * Format of the file is auto-detected from the file extension. * Supported file formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the file extension is unsupported. * * @param file specified file * @param delayTime delay to observe between every check. The default value is 5. * @param unit time unit of delay. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated file is loaded * @return a child config containing values from watched file * @throws UnsupportedExtensionException */ fun watchFile( file: File, delayTime: Long = 5, unit: TimeUnit = TimeUnit.SECONDS, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config = dispatchExtension(file.extension, file.name) .watchFile(file, delayTime, unit, context, optional, onLoad) /** * Returns a child config containing values from specified file path, * and reloads values when file content has been changed. * * Format of the file is auto-detected from the file extension. * Supported file formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the file extension is unsupported. * * @param file specified file path * @param delayTime delay to observe between every check. The default value is 5. * @param unit time unit of delay. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated file is loaded * @return a child config containing values from watched file * @throws UnsupportedExtensionException */ fun watchFile( file: String, delayTime: Long = 5, unit: TimeUnit = TimeUnit.SECONDS, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config = watchFile(File(file), delayTime, unit, context, optional, onLoad) /** * Returns a child config containing values from specified url. * * Format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the url extension is unsupported. * * @param url specified url * @param optional whether the source is optional * @return a child config containing values from specified url * @throws UnsupportedExtensionException */ fun url(url: URL, optional: Boolean = this.optional): Config = dispatchExtension(File(url.path).extension, url.toString()).url(url, optional) /** * Returns a child config containing values from specified url string. * * Format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the url extension is unsupported. * * @param url specified url string * @param optional whether the source is optional * @return a child config containing values from specified url string * @throws UnsupportedExtensionException */ fun url(url: String, optional: Boolean = this.optional): Config = url(URL(url), optional) /** * Returns a child config containing values from specified url, * and reloads values periodically. * * Format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the url extension is unsupported. * * @param url specified url * @param period reload period. The default value is 5. * @param unit time unit of delay. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated URL is loaded * @return a child config containing values from specified url * @throws UnsupportedExtensionException */ fun watchUrl( url: URL, period: Long = 5, unit: TimeUnit = TimeUnit.SECONDS, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config = dispatchExtension(File(url.path).extension, url.toString()) .watchUrl(url, period, unit, context, optional, onLoad) /** * Returns a child config containing values from specified url string, * and reloads values periodically. * * Format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the url extension is unsupported. * * @param url specified url string * @param period reload period. The default value is 5. * @param unit time unit of delay. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated URL is loaded * @return a child config containing values from specified url string * @throws UnsupportedExtensionException */ fun watchUrl( url: String, period: Long = 5, unit: TimeUnit = TimeUnit.SECONDS, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config = watchUrl(URL(url), period, unit, context, optional, onLoad) } /** * Loader to load source from map of variant formats. * * If [transform] is provided, source will be applied the given [transform] function when loaded. * * @param config parent config */ class MapLoader( /** * Parent config for all child configs loading source in this loader. */ val config: Config, /** * The given transformation function. */ private val transform: ((Source) -> Source)? = null ) { fun Source.orMapped(): Source = transform?.invoke(this) ?: this /** * Returns a child config containing values from specified hierarchical map. * * @param map a hierarchical map * @return a child config containing values from specified hierarchical map */ fun hierarchical(map: Map): Config = config.withSource(MapSource(map).orMapped()) /** * Returns a child config containing values from specified map in key-value format. * * @param map a map in key-value format * @return a child config containing values from specified map in key-value format */ fun kv(map: Map): Config = config.withSource(KVSource(map).orMapped()) /** * Returns a child config containing values from specified map in flat format. * * @param map a map in flat format * @return a child config containing values from specified map in flat format */ fun flat(map: Map): Config = config.withSource(FlatSource(map).orMapped()) } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/DefaultProviders.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.base.FlatSource import com.uchuhimo.konf.source.base.KVSource import com.uchuhimo.konf.source.base.MapSource import com.uchuhimo.konf.source.env.EnvProvider import com.uchuhimo.konf.source.json.JsonProvider import com.uchuhimo.konf.source.properties.PropertiesProvider import java.io.File import java.net.URL /** * Default providers. */ object DefaultProviders { /** * Provider for JSON source. */ @JvmField val json = JsonProvider /** * Provider for properties source. */ @JvmField val properties = PropertiesProvider /** * Provider for map source. */ @JvmField val map = DefaultMapProviders /** * Returns a source from system environment. * * @param nested whether to treat "AA_BB_CC" as nested format "AA.BB.CC" or not. True by default. * @return a source from system environment */ @JvmOverloads fun env(nested: Boolean = true): Source = EnvProvider.env(nested) /** * Returns a source from system properties. * * @return a source from system properties */ fun systemProperties(): Source = PropertiesProvider.system() /** * Returns corresponding provider based on extension. * * @param extension the file extension * @param source the source description for error message * @return the corresponding provider based on extension */ fun dispatchExtension(extension: String, source: String = ""): Provider = Provider.of(extension) ?: throw UnsupportedExtensionException(source) /** * Returns a source from specified file. * * Format of the file is auto-detected from the file extension. * Supported file formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the file extension is unsupported. * * @param file specified file * @param optional whether the source is optional * @return a source from specified file * @throws UnsupportedExtensionException */ fun file(file: File, optional: Boolean = false): Source = dispatchExtension(file.extension, file.name).file(file, optional) /** * Returns a source from specified file path. * * Format of the file is auto-detected from the file extension. * Supported file formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the file extension is unsupported. * * @param file specified file path * @param optional whether the source is optional * @return a source from specified file path * @throws UnsupportedExtensionException */ fun file(file: String, optional: Boolean = false): Source = file(File(file), optional) /** * Returns a source from specified url. * * Format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the url extension is unsupported. * * @param url specified url * @param optional whether the source is optional * @return a source from specified url * @throws UnsupportedExtensionException */ fun url(url: URL, optional: Boolean = false): Source = dispatchExtension(File(url.path).extension, url.toString()).url(url, optional) /** * Returns a source from specified url string. * * Format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the url extension is unsupported. * * @param url specified url string * @param optional whether the source is optional * @return a source from specified url string * @throws UnsupportedExtensionException */ fun url(url: String, optional: Boolean = false): Source = url(URL(url), optional) } /** * Providers for map of variant formats. */ object DefaultMapProviders { /** * Returns a source from specified hierarchical map. * * @param map a hierarchical map * @return a source from specified hierarchical map */ fun hierarchical(map: Map): Source = MapSource(map) /** * Returns a source from specified map in key-value format. * * @param map a map in key-value format * @return a source from specified map in key-value format */ fun kv(map: Map): Source = KVSource(map) /** * Returns a source from specified map in flat format. * * @param map a map in flat format * @return a source from specified map in flat format */ fun flat(map: Map): Source = FlatSource(map) } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/Loader.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.io.File import java.io.InputStream import java.io.Reader import java.net.URL import java.nio.file.FileSystems import java.nio.file.Path import java.nio.file.StandardWatchEventKinds import java.nio.file.WatchEvent import java.security.DigestInputStream import java.security.MessageDigest import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext /** * Loader to load source from various input formats. * * @param config parent config */ class Loader( /** * Parent config for all child configs loading source in this loader. */ val config: Config, /** * Source provider to provide source from various input format. */ val provider: Provider ) { val optional = config.isEnabled(Feature.OPTIONAL_SOURCE_BY_DEFAULT) /** * Returns a child config containing values from specified reader. * * @param reader specified reader for reading character streams * @return a child config containing values from specified reader */ fun reader(reader: Reader): Config = config.withSource(provider.reader(reader)) /** * Returns a child config containing values from specified input stream. * * @param inputStream specified input stream of bytes * @return a child config containing values from specified input stream */ fun inputStream(inputStream: InputStream): Config = config.withSource(provider.inputStream(inputStream)) /** * Returns a child config containing values from specified file. * * @param file specified file * @param optional whether the source is optional * @return a child config containing values from specified file */ fun file(file: File, optional: Boolean = this.optional): Config = config.withSource(provider.file(file, optional)) /** * Returns a child config containing values from specified file path. * * @param file specified file path * @param optional whether the source is optional * @return a child config containing values from specified file path */ fun file(file: String, optional: Boolean = this.optional): Config = config.withSource(provider.file(file, optional)) private val File.digest: ByteArray get() { val messageDigest = MessageDigest.getInstance("MD5") DigestInputStream(inputStream().buffered(), messageDigest).use { it.readBytes() } return messageDigest.digest() } /** * Returns a child config containing values from specified file, * and reloads values when file content has been changed. * * @param file specified file * @param delayTime delay to observe between every check. The default value is 5. * @param unit time unit of delay. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated file is loaded * @return a child config containing values from watched file */ fun watchFile( file: File, delayTime: Long = 5, unit: TimeUnit = TimeUnit.SECONDS, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config { val absoluteFile = file.absoluteFile return provider.file(absoluteFile, optional).let { source -> config.withLoadTrigger("watch ${source.description}") { newConfig, load -> newConfig.lock { load(source) } onLoad?.invoke(newConfig, source) val path = absoluteFile.toPath().parent val isMac = "mac" in System.getProperty("os.name").toLowerCase() val watcher = FileSystems.getDefault().newWatchService() path.register( watcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE ) var digest = absoluteFile.digest GlobalScope.launch(context) { while (true) { delay(unit.toMillis(delayTime)) if (isMac) { val newDigest = absoluteFile.digest if (!newDigest.contentEquals(digest)) { digest = newDigest val newSource = provider.file(file, optional) newConfig.lock { newConfig.clear() load(newSource) } onLoad?.invoke(newConfig, newSource) } } else { val key = watcher.poll() if (key != null) { for (event in key.pollEvents()) { val kind = event.kind() @Suppress("UNCHECKED_CAST") event as WatchEvent val filename = event.context() if (filename.toString() == absoluteFile.name) { if (kind == StandardWatchEventKinds.OVERFLOW) { continue } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY || kind == StandardWatchEventKinds.ENTRY_CREATE ) { val newSource = provider.file(file, optional) newConfig.lock { newConfig.clear() load(newSource) } onLoad?.invoke(newConfig, newSource) } } val valid = key.reset() if (!valid) { watcher.close() throw InvalidWatchKeyException(source) } } } } } } }.withLayer() } } /** * Returns a child config containing values from specified file path, * and reloads values when file content has been changed. * * @param file specified file path * @param delayTime delay to observe between every check. The default value is 5. * @param unit time unit of delay. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated file is loaded * @return a child config containing values from watched file */ fun watchFile( file: String, delayTime: Long = 5, unit: TimeUnit = TimeUnit.SECONDS, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config = watchFile(File(file), delayTime, unit, context, optional, onLoad) /** * Returns a child config containing values from specified string. * * @param content specified string * @return a child config containing values from specified string */ fun string(content: String): Config = config.withSource(provider.string(content)) /** * Returns a child config containing values from specified byte array. * * @param content specified byte array * @return a child config containing values from specified byte array */ fun bytes(content: ByteArray): Config = config.withSource(provider.bytes(content)) /** * Returns a child config containing values from specified portion of byte array. * * @param content specified byte array * @param offset the start offset of the portion of the array to read * @param length the length of the portion of the array to read * @return a child config containing values from specified portion of byte array */ fun bytes(content: ByteArray, offset: Int, length: Int): Config = config.withSource(provider.bytes(content, offset, length)) /** * Returns a child config containing values from specified url. * * @param url specified url * @param optional whether the source is optional * @return a child config containing values from specified url */ fun url(url: URL, optional: Boolean = this.optional): Config = config.withSource(provider.url(url, optional)) /** * Returns a child config containing values from specified url string. * * @param url specified url string * @param optional whether the source is optional * @return a child config containing values from specified url string */ fun url(url: String, optional: Boolean = this.optional): Config = config.withSource(provider.url(url, optional)) /** * Returns a child config containing values from specified url, * and reloads values periodically. * * @param url specified url * @param period reload period. The default value is 5. * @param unit time unit of reload period. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated URL is loaded * @return a child config containing values from specified url */ fun watchUrl( url: URL, period: Long = 5, unit: TimeUnit = TimeUnit.SECONDS, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config { return provider.url(url, optional).let { source -> config.withLoadTrigger("watch ${source.description}") { newConfig, load -> newConfig.lock { load(source) } onLoad?.invoke(newConfig, source) GlobalScope.launch(context) { while (true) { delay(unit.toMillis(period)) val newSource = provider.url(url, optional) newConfig.lock { newConfig.clear() load(newSource) } onLoad?.invoke(newConfig, newSource) } } }.withLayer() } } /** * Returns a child config containing values from specified url string, * and reloads values periodically. * * @param url specified url string * @param period reload period. The default value is 5. * @param unit time unit of reload period. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated URL is loaded * @return a child config containing values from specified url string */ fun watchUrl( url: String, period: Long = 5, unit: TimeUnit = TimeUnit.SECONDS, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config = watchUrl(URL(url), period, unit, context, optional, onLoad) /** * Returns a child config containing values from specified resource. * * @param resource path of specified resource * @param optional whether the source is optional * @return a child config containing values from specified resource */ fun resource(resource: String, optional: Boolean = this.optional): Config = config.withSource(provider.resource(resource, optional)) } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/MergedSource.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.Feature import com.uchuhimo.konf.MergedMap import com.uchuhimo.konf.Path import com.uchuhimo.konf.TreeNode import java.util.Collections class MergedSource(val facade: Source, val fallback: Source) : Source { override val info: SourceInfo by lazy { SourceInfo( "facade" to facade.description, "fallback" to fallback.description ) } override val tree: TreeNode by lazy { facade.tree.withFallback(fallback.tree) } override val features: Map by lazy { MergedMap( Collections.unmodifiableMap(fallback.features), Collections.unmodifiableMap(facade.features) ) } override fun disabled(feature: Feature): Source = MergedSource(facade.disabled(feature), fallback) override fun enabled(feature: Feature): Source = MergedSource(facade.enabled(feature), fallback) override fun substituted(root: Source, enabled: Boolean, errorWhenUndefined: Boolean): Source { val substitutedFacade = facade.substituted(root, enabled, errorWhenUndefined) val substitutedFallback = fallback.substituted(root, enabled, errorWhenUndefined) if (substitutedFacade === facade && substitutedFallback === fallback) { return this } else { return MergedSource(substitutedFacade, substitutedFallback) } } override fun lowercased(enabled: Boolean): Source { val lowercasedFacade = facade.lowercased(enabled) val lowercasedFallback = fallback.lowercased(enabled) if (lowercasedFacade === facade && lowercasedFallback === fallback) { return this } else { return MergedSource(lowercasedFacade, lowercasedFallback) } } override fun littleCamelCased(enabled: Boolean): Source { val littleCamelCasedFacade = facade.littleCamelCased(enabled) val littleCamelCasedFallback = fallback.littleCamelCased(enabled) if (littleCamelCasedFacade === facade && littleCamelCasedFallback === fallback) { return this } else { return MergedSource(littleCamelCasedFacade, littleCamelCasedFallback) } } override fun normalized(lowercased: Boolean, littleCamelCased: Boolean): Source { val normalizedFacade = facade.normalized(lowercased, littleCamelCased) val normalizedFallback = fallback.normalized(lowercased, littleCamelCased) if (normalizedFacade === facade && normalizedFallback === fallback) { return this } else { return MergedSource(normalizedFacade, normalizedFallback) } } override fun getNodeOrNull(path: Path, lowercased: Boolean, littleCamelCased: Boolean): TreeNode? { val facadeNode = facade.getNodeOrNull(path, lowercased, littleCamelCased) val fallbackNode = fallback.getNodeOrNull(path, lowercased, littleCamelCased) return if (facadeNode != null) { if (fallbackNode != null) { facadeNode.withFallback(fallbackNode) } else { facadeNode } } else { fallbackNode } } override fun getOrNull(path: Path): Source? { return if (path.isEmpty()) { this } else { val subFacade = facade.getOrNull(path) val subFallback = fallback.getOrNull(path) if (subFacade != null) { if (subFallback != null) { MergedSource(subFacade, subFallback) } else { subFacade } } else { subFallback } } } override fun withPrefix(prefix: Path): Source { return if (prefix.isEmpty()) { this } else { MergedSource(facade.withPrefix(prefix), fallback.withPrefix(prefix)) } } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/Provider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.base.EmptyMapSource import com.uchuhimo.konf.source.json.JsonProvider import com.uchuhimo.konf.source.properties.PropertiesProvider import org.reflections.ReflectionUtils import org.reflections.Reflections import org.reflections.scanners.SubTypesScanner import org.reflections.scanners.TypeAnnotationsScanner import java.io.File import java.io.IOException import java.io.InputStream import java.io.Reader import java.net.URL import java.util.concurrent.ConcurrentHashMap /** * Provides source from various input format. */ interface Provider { /** * Returns a new source from specified reader. * * @param reader specified reader for reading character streams * @return a new source from specified reader */ fun reader(reader: Reader): Source /** * Returns a new source from specified input stream. * * @param inputStream specified input stream of bytes * @return a new source from specified input stream */ fun inputStream(inputStream: InputStream): Source /** * Returns a new source from specified file. * * @param file specified file * @param optional whether this source is optional * @return a new source from specified file */ fun file(file: File, optional: Boolean = false): Source { val extendContext: Source.() -> Unit = { info["file"] = file.toString() } if (!file.exists() && optional) { return EmptyMapSource().apply(extendContext) } return file.inputStream().buffered().use { inputStream -> inputStream(inputStream).apply(extendContext) } } /** * Returns a new source from specified file path. * * @param file specified file path * @param optional whether this source is optional * @return a new source from specified file path */ fun file(file: String, optional: Boolean = false): Source = file(File(file), optional) /** * Returns a new source from specified string. * * @param content specified string * @return a new source from specified string */ fun string(content: String): Source = reader(content.reader()).apply { info["content"] = "\"\n$content\n\"" } /** * Returns a new source from specified byte array. * * @param content specified byte array * @return a new source from specified byte array */ fun bytes(content: ByteArray): Source { return content.inputStream().use { inputStream(it) } } /** * Returns a new source from specified portion of byte array. * * @param content specified byte array * @param offset the start offset of the portion of the array to read * @param length the length of the portion of the array to read * @return a new source from specified portion of byte array */ fun bytes(content: ByteArray, offset: Int, length: Int): Source { return content.inputStream(offset, length).use { inputStream(it) } } /** * Returns a new source from specified url. * * @param url specified url * @param optional whether this source is optional * @return a new source from specified url */ fun url(url: URL, optional: Boolean = false): Source { // from com.fasterxml.jackson.core.JsonFactory._optimizedStreamFromURL in version 2.8.9 val extendContext: Source.() -> Unit = { info["url"] = url.toString() } if (url.protocol == "file") { val host = url.host if (host == null || host.isEmpty()) { val path = url.path if (path.indexOf('%') < 0) { val file = File(path) if (!file.exists() && optional) { return EmptyMapSource().apply(extendContext) } return file.inputStream().use { inputStream(it).apply(extendContext) } } } } return try { val stream = url.openStream() stream.use { inputStream(it).apply(extendContext) } } catch (ex: IOException) { if (optional) { EmptyMapSource().apply(extendContext) } else { throw ex } } } /** * Returns a new source from specified url string. * * @param url specified url string * @param optional whether this source is optional * @return a new source from specified url string */ fun url(url: String, optional: Boolean = false): Source = url(URL(url), optional) /** * Returns a new source from specified resource. * * @param resource path of specified resource * @param optional whether this source is optional * @return a new source from specified resource */ fun resource(resource: String, optional: Boolean = false): Source { val extendContext: Source.() -> Unit = { info["resource"] = resource } val loader = Thread.currentThread().contextClassLoader val e = try { loader.getResources(resource) } catch (ex: IOException) { if (optional) { return EmptyMapSource().apply(extendContext) } else { throw ex } } if (!e.hasMoreElements()) { if (optional) { return EmptyMapSource().apply(extendContext) } else { throw SourceNotFoundException("resource not found on classpath: $resource") } } val sources = mutableListOf() while (e.hasMoreElements()) { val url = e.nextElement() val source = url(url, optional) sources.add(source) } return sources.reduce(Source::withFallback).apply(extendContext) } /** * Returns a provider providing sources that applying the given [transform] function. * * @param transform the given transformation function * @return a provider providing sources that applying the given [transform] function */ fun map(transform: (Source) -> Source): Provider { return object : Provider { override fun reader(reader: Reader): Source = this@Provider.reader(reader).let(transform) override fun inputStream(inputStream: InputStream): Source = this@Provider.inputStream(inputStream).let(transform) override fun file(file: File, optional: Boolean): Source = this@Provider.file(file, optional).let(transform) override fun file(file: String, optional: Boolean): Source = this@Provider.file(file, optional).let(transform) override fun string(content: String): Source = this@Provider.string(content).let(transform) override fun bytes(content: ByteArray): Source = this@Provider.bytes(content).let(transform) override fun bytes(content: ByteArray, offset: Int, length: Int): Source = this@Provider.bytes(content, offset, length).let(transform) override fun url(url: URL, optional: Boolean): Source = this@Provider.url(url, optional).let(transform) override fun url(url: String, optional: Boolean): Source = this@Provider.url(url, optional).let(transform) override fun resource(resource: String, optional: Boolean): Source = this@Provider.resource(resource, optional).let(transform) } } companion object { private val extensionToProvider = ConcurrentHashMap( mutableMapOf( "json" to JsonProvider, "properties" to PropertiesProvider ) ) init { val reflections = Reflections( "com.uchuhimo.konf", SubTypesScanner(), TypeAnnotationsScanner() ) val providers = reflections.getSubTypesOf(Provider::class.java) .intersect(reflections.getTypesAnnotatedWith(RegisterExtension::class.java)) for (provider in providers) { for ( annotation in ReflectionUtils.getAnnotations(provider).filter { it.annotationClass == RegisterExtension::class } ) { for (extension in (annotation as RegisterExtension).value) { registerExtension(extension, provider.kotlin.objectInstance!! as Provider) } } } } /** * Register extension with the corresponding provider. * * @param extension the file extension * @param provider the corresponding provider */ fun registerExtension(extension: String, provider: Provider) { extensionToProvider[extension] = provider } /** * Unregister the given extension. * * @param extension the file extension */ fun unregisterExtension(extension: String): Provider? = extensionToProvider.remove(extension) /** * Returns corresponding provider based on extension. * * Returns null if the specific extension is unregistered. * * @param extension the file extension * @return the corresponding provider based on extension */ fun of(extension: String): Provider? = extensionToProvider[extension] } } @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.CLASS) annotation class RegisterExtension(val value: Array) ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.BigIntegerNode import com.fasterxml.jackson.databind.node.BooleanNode import com.fasterxml.jackson.databind.node.DecimalNode import com.fasterxml.jackson.databind.node.DoubleNode import com.fasterxml.jackson.databind.node.FloatNode import com.fasterxml.jackson.databind.node.IntNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.LongNode import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.ShortNode import com.fasterxml.jackson.databind.node.TextNode import com.fasterxml.jackson.databind.node.TreeTraversingParser import com.fasterxml.jackson.databind.type.ArrayType import com.fasterxml.jackson.databind.type.CollectionLikeType import com.fasterxml.jackson.databind.type.MapLikeType import com.fasterxml.jackson.databind.type.SimpleType import com.fasterxml.jackson.databind.type.TypeFactory import com.fasterxml.jackson.module.kotlin.convertValue import com.uchuhimo.konf.Config import com.uchuhimo.konf.ContainerNode import com.uchuhimo.konf.EmptyNode import com.uchuhimo.konf.Feature import com.uchuhimo.konf.Item import com.uchuhimo.konf.ListNode import com.uchuhimo.konf.MapNode import com.uchuhimo.konf.MergedMap import com.uchuhimo.konf.NullNode import com.uchuhimo.konf.Path import com.uchuhimo.konf.SizeInBytes import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.base.ListStringNode import com.uchuhimo.konf.source.base.toHierarchical import com.uchuhimo.konf.toLittleCamelCase import com.uchuhimo.konf.toPath import com.uchuhimo.konf.toTree import com.uchuhimo.konf.toValue import org.apache.commons.text.StringSubstitutor import org.apache.commons.text.lookup.StringLookup import org.apache.commons.text.lookup.StringLookupFactory import java.lang.reflect.InvocationTargetException import java.math.BigDecimal import java.math.BigInteger import java.time.Duration import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.OffsetDateTime import java.time.OffsetTime import java.time.Year import java.time.YearMonth import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeParseException import java.util.ArrayDeque import java.util.Collections import java.util.Date import java.util.Queue import java.util.SortedMap import java.util.SortedSet import java.util.TreeMap import java.util.TreeSet import java.util.regex.Pattern import kotlin.Byte import kotlin.Char import kotlin.Double import kotlin.Float import kotlin.Int import kotlin.Long import kotlin.Short import kotlin.String import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.starProjectedType import com.fasterxml.jackson.databind.node.NullNode as JacksonNullNode /** * Source to provide values for config. * * When config loads values from source, config will iterate all items in it, and * retrieve value with path of each item from source. * When source contains single value, a series of `is` operations can be used to * judge the actual type of value, and `to` operation can be used to get the value * with specified type. * When source contains multiple value, `contains` operations can be used to check * whether value(s) in specified path is in this source, and `get` operations can be used * to retrieve the corresponding sub-source. */ interface Source { /** * Description of this source. */ val description: String get() = this.info.map { (name, value) -> "$name: $value" }.joinToString(separator = ", ", prefix = "[", postfix = "]") /** * Information about this source. * * Info is in form of key-value pairs. */ val info: SourceInfo /** * a tree node that represents internal structure of this source. */ val tree: TreeNode /** * Feature flags in this source. */ val features: Map get() = emptyMap() /** * Whether this source contains value(s) in specified path or not. * * @param path item path * @return `true` if this source contains value(s) in specified path, `false` otherwise */ operator fun contains(path: Path): Boolean = path in tree /** * Returns sub-source in specified path if this source contains value(s) in specified path, * `null` otherwise. * * @param path item path * @return sub-source in specified path if this source contains value(s) in specified path, * `null` otherwise */ fun getOrNull(path: Path): Source? { return if (path.isEmpty()) { this } else { getTreeOrNull(tree, normalizedPath(path))?.let { Source(info = info, tree = it, features = features) } } } private fun getTreeOrNull(tree: TreeNode, path: Path): TreeNode? { return if (path.isEmpty()) { tree } else { val key = normalizedKey(path.first()) val rest = path.drop(1) var result: TreeNode? = null for ((childKey, child) in tree.children) { if (key == normalizedKey(childKey)) { result = child break } } result?.let { getTreeOrNull(it, rest) } } } private fun normalizedKey(key: String): String { var currentKey = key if (isEnabled(Feature.LOAD_KEYS_AS_LITTLE_CAMEL_CASE)) { currentKey = currentKey.toLittleCamelCase() } if (isEnabled(Feature.LOAD_KEYS_CASE_INSENSITIVELY)) { currentKey = currentKey.toLowerCase() } return currentKey } private fun normalizedPath(path: Path, lowercased: Boolean = false, littleCamelCased: Boolean = true): Path { var currentPath = path if (littleCamelCased && isEnabled(Feature.LOAD_KEYS_AS_LITTLE_CAMEL_CASE)) { currentPath = currentPath.map { it.toLittleCamelCase() } } if (lowercased || isEnabled(Feature.LOAD_KEYS_CASE_INSENSITIVELY)) { currentPath = currentPath.map { it.toLowerCase() } } return currentPath } fun getNodeOrNull(path: Path, lowercased: Boolean = false, littleCamelCased: Boolean = true): TreeNode? { return tree.getOrNull(normalizedPath(path, lowercased, littleCamelCased)) } /** * Returns sub-source in specified path. * * Throws [NoSuchPathException] if there is no value in specified path. * * @param path item path * @return sub-source in specified path * @throws NoSuchPathException */ operator fun get(path: Path): Source = getOrNull(path) ?: throw NoSuchPathException(this, path) /** * Whether this source contains value(s) with specified prefix or not. * * @param prefix item prefix * @return `true` if this source contains value(s) with specified prefix, `false` otherwise */ operator fun contains(prefix: String): Boolean = contains(prefix.toPath()) /** * Returns sub-source in specified path if this source contains value(s) in specified path, * `null` otherwise. * * @param path item path * @return sub-source in specified path if this source contains value(s) in specified path, * `null` otherwise */ fun getOrNull(path: String): Source? = getOrNull(path.toPath()) /** * Returns sub-source in specified path. * * Throws [NoSuchPathException] if there is no value in specified path. * * @param path item path * @return sub-source in specified path * @throws NoSuchPathException */ operator fun get(path: String): Source = get(path.toPath()) /** * Returns source with specified additional prefix. * * @param prefix additional prefix * @return source with specified additional prefix */ fun withPrefix(prefix: Path): Source { return if (prefix.isEmpty()) { this } else { var prefixedTree = tree for (key in prefix.asReversed()) { prefixedTree = ContainerNode(mutableMapOf(key to prefixedTree)) } Source( info = this@Source.info, tree = prefixedTree, features = features ) } } /** * Returns source with specified additional prefix. * * @param prefix additional prefix * @return source with specified additional prefix */ fun withPrefix(prefix: String): Source = withPrefix(prefix.toPath()) /** * Returns a source backing by specified fallback source. * * When config fails to retrieve values from this source, it will try to retrieve them from * fallback source. * * @param fallback fallback source * @return a source backing by specified fallback source */ fun withFallback(fallback: Source): Source = MergedSource(this, fallback) /** * Returns a source overlapped by the specified facade source. * * When config fails to retrieve values from the facade source, it will try to retrieve them * from this source. * * @param facade the facade source * @return a source overlapped by the specified facade source */ operator fun plus(facade: Source): Source = facade.withFallback(this) /** * Return a source that substitutes path variables within all strings by values. * * See [StringSubstitutor](https://commons.apache.org/proper/commons-text/apidocs/org/apache/commons/text/StringSubstitutor.html) * for detailed substitution rules. An exception is when the string is in reference format like `${path}`, * the whole node will be replace by a reference to the sub-tree in the specified path. * * @param root the root source for substitution * @param enabled whether enabled or let the source decide by itself * @param errorWhenUndefined whether throw exception when this source contains undefined path variables * @return a source that substitutes path variables within all strings by values * @throws UndefinedPathVariableException */ fun substituted(root: Source = this, enabled: Boolean = true, errorWhenUndefined: Boolean = true): Source { return if (!enabled || !this.isEnabled(Feature.SUBSTITUTE_SOURCE_BEFORE_LOADED)) { this } else { Source(info, tree.substituted(root, errorWhenUndefined), features) } } fun lowercased(enabled: Boolean = false): Source { return if (enabled || this.isEnabled(Feature.LOAD_KEYS_CASE_INSENSITIVELY)) { Source(info, tree.lowercased(), features) } else { this } } fun littleCamelCased(enabled: Boolean = true): Source { return if (!enabled || !this.isEnabled(Feature.LOAD_KEYS_AS_LITTLE_CAMEL_CASE)) { this } else { Source(info, tree.littleCamelCased(), features) } } fun normalized(lowercased: Boolean = false, littleCamelCased: Boolean = true): Source { var currentSource = this currentSource = currentSource.littleCamelCased(littleCamelCased) currentSource = currentSource.lowercased(lowercased) return currentSource } /** * Returns a new source that enables the specified feature. * * @param feature the specified feature * @return a new source */ fun enabled(feature: Feature): Source = Source( info, tree, MergedMap(Collections.unmodifiableMap(features), mutableMapOf(feature to true)) ) /** * Returns a new source that disables the specified feature. * * @param feature the specified feature * @return a new source */ fun disabled(feature: Feature): Source = Source( info, tree, MergedMap(Collections.unmodifiableMap(features), mutableMapOf(feature to false)) ) /** * Check whether the specified feature is enabled or not. * * @param feature the specified feature * @return whether the specified feature is enabled or not */ fun isEnabled(feature: Feature): Boolean = features[feature] ?: feature.enabledByDefault companion object { operator fun invoke( info: SourceInfo = SourceInfo(), tree: TreeNode = ContainerNode.empty(), features: Map = emptyMap() ): Source { return BaseSource(info, tree, features) } /** * Returns default providers. * * It is a fluent API for default providers. */ val from = DefaultProviders /** * Returns default providers. * * It is a fluent API for default providers. * * @return default providers. */ @JavaApi @JvmStatic fun from() = from } } /** * Returns a value casted from source. * * @return a value casted from source */ inline fun Source.toValue(): T { return Config().withSource(this).toValue() } private val singleVariablePattern = Pattern.compile("^\\$\\{(.+)}$") private fun TreeNode.substituted( source: Source, errorWhenUndefined: Boolean, lookup: TreeLookup = TreeLookup(source.tree, source, errorWhenUndefined) ): TreeNode { when (this) { is NullNode -> return this is ValueNode -> { if (this is SubstitutableNode && value is String) { val text = (if (substituted) originalValue else value) as String val matcher = singleVariablePattern.matcher(text.trim()) if (matcher.find()) { val matchedValue = matcher.group(1) try { val resolvedValue = lookup.replace(matchedValue) val node = lookup.root.getOrNull(resolvedValue) if (node != null) { return node.substituted(source, true, lookup) } } catch (_: Exception) { } } try { return substitute(lookup.replace(text)) } catch (_: IllegalArgumentException) { throw UndefinedPathVariableException(source, text) } } else { return this } } is ListNode -> { return withList(list.map { it.substituted(source, errorWhenUndefined, lookup) }) } is MapNode -> { return withMap( children.mapValues { (_, child) -> child.substituted(source, errorWhenUndefined, lookup) } ) } else -> throw UnsupportedNodeTypeException(source, this) } } private fun TreeNode.lowercased(): TreeNode { if (this is ContainerNode) { return withMap( children.mapKeys { (key, _) -> key.toLowerCase() }.mapValues { (_, child) -> child.lowercased() } ) } else { return this } } private fun TreeNode.littleCamelCased(): TreeNode { if (this is ContainerNode) { return withMap( children.mapKeys { (key, _) -> key.toLittleCamelCase() }.mapValues { (_, child) -> child.littleCamelCased() } ) } else { return this } } class TreeLookup(val root: TreeNode, val source: Source, errorWhenUndefined: Boolean) : StringLookup { val substitutor: StringSubstitutor = StringSubstitutor( StringLookupFactory.INSTANCE.interpolatorStringLookup(this) ).apply { isEnableSubstitutionInVariables = true isEnableUndefinedVariableException = errorWhenUndefined } override fun lookup(key: String): String? { val node = root.getOrNull(key) if (node != null && node is ValueNode) { if (node.value::class in listOf( String::class, Char::class, Byte::class, Short::class, Int::class, Long::class, BigInteger::class ) ) { val value = node.value.toString() return substitutor.replace(value) } else { throw WrongTypeException( "${node.value} in ${source.description}", node.value::class.java.simpleName, "String" ) } } else { return null } } fun replace(text: String): String { return substitutor.replace(text) } } open class BaseSource( override val info: SourceInfo = SourceInfo(), override val tree: TreeNode = ContainerNode.empty(), override val features: Map = emptyMap() ) : Source /** * Information of source for debugging. */ class SourceInfo( private val info: MutableMap = mutableMapOf() ) : MutableMap by info { constructor(vararg pairs: Pair) : this(mutableMapOf(*pairs)) fun with(vararg pairs: Pair): SourceInfo { return SourceInfo(MergedMap(fallback = this, facade = mutableMapOf(*pairs))) } fun with(sourceInfo: SourceInfo): SourceInfo { return SourceInfo(MergedMap(fallback = this, facade = sourceInfo.toMutableMap())) } } inline fun Source.asValue(): T { return tree.asValueOf(this, T::class.java) as T } fun TreeNode.asValueOf(source: Source, type: Class<*>): Any { return castOrNull(source, type) ?: throw WrongTypeException( if (this is ValueNode) "${this.value} in ${source.description}" else "$this in ${source.description}", if (this is ValueNode) this.value::class.java.simpleName else "Unknown", type.simpleName ) } internal fun Any?.toCompatibleValue(mapper: ObjectMapper): Any { return when (this) { is OffsetTime, is OffsetDateTime, is ZonedDateTime, is LocalDate, is LocalDateTime, is LocalTime, is Year, is YearMonth, is Instant, is Duration -> this.toString() is Date -> this.toInstant().toString() is SizeInBytes -> this.bytes.toString() is Enum<*> -> this.name is ByteArray -> this.toList() is CharArray -> this.toList().map { it.toString() } is BooleanArray -> this.toList() is IntArray -> this.toList() is ShortArray -> this.toList() is LongArray -> this.toList() is DoubleArray -> this.toList() is FloatArray -> this.toList() is List<*> -> this.map { it!!.toCompatibleValue(mapper) } is Set<*> -> this.map { it!!.toCompatibleValue(mapper) } is Array<*> -> this.map { it!!.toCompatibleValue(mapper) } is Map<*, *> -> this.mapValues { (_, value) -> value.toCompatibleValue(mapper) } is Char -> this.toString() is String, is Boolean, is Int, is Short, is Byte, is Long, is BigInteger, is Double, is Float, is BigDecimal -> this else -> { if (this == null) { "null" } else { mapper.convertValue(this).toCompatibleValue(mapper) } } } } internal fun Config.loadItem(item: Item<*>, path: Path, source: Source): Boolean { try { val itemNode = source.getNodeOrNull( path, lowercased = this.isEnabled(Feature.LOAD_KEYS_CASE_INSENSITIVELY), littleCamelCased = this.isEnabled(Feature.LOAD_KEYS_AS_LITTLE_CAMEL_CASE) ) if (itemNode != null && !itemNode.isPlaceHolderNode()) { if (item.nullable && ( (itemNode is NullNode) || (itemNode is ValueNode && itemNode.value == "null") ) ) { rawSet(item, null) } else { rawSet(item, itemNode.toValue(source, item.type, mapper)) } return true } else { return false } } catch (cause: SourceException) { throw LoadException(path, cause) } } internal fun load(config: Config, source: Source): Source { var currentSource = source currentSource = currentSource.normalized( lowercased = config.isEnabled(Feature.LOAD_KEYS_CASE_INSENSITIVELY), littleCamelCased = config.isEnabled(Feature.LOAD_KEYS_AS_LITTLE_CAMEL_CASE) ) currentSource = currentSource.substituted( enabled = config.isEnabled(Feature.SUBSTITUTE_SOURCE_BEFORE_LOADED) ) config.lock { for (item in config) { config.loadItem(item, config.pathOf(item), currentSource) } if (currentSource.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH) || config.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH) ) { val treeFromSource = currentSource.tree val treeFromConfig = config.toTree() val diffTree = treeFromSource - treeFromConfig if (diffTree != EmptyNode) { val unknownPaths = diffTree.paths throw UnknownPathsException(currentSource, unknownPaths) } } } return currentSource } private inline fun TreeNode.cast(source: Source): T { if (this !is ValueNode) { throw WrongTypeException("$this in ${source.description}", this::class.java.simpleName, T::class.java.simpleName) } if (T::class.java.isInstance(value)) { return value as T } else { throw WrongTypeException("$value in ${source.description}", value::class.java.simpleName, T::class.java.simpleName) } } internal fun stringToBoolean(value: String): Boolean { return when { value.toLowerCase() == "true" -> true value.toLowerCase() == "false" -> false else -> throw ParseException("$value cannot be parsed to a boolean") } } internal fun shortToByte(value: Short): Byte { if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { throw ParseException("$value cannot be parsed to a byte") } return value.toByte() } internal fun intToShort(value: Int): Short { if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { throw ParseException("$value cannot be parsed to a short") } return value.toShort() } internal fun longToInt(value: Long): Int { if (value < Int.MIN_VALUE || value > Int.MAX_VALUE) { throw ParseException("$value cannot be parsed to an int") } return value.toInt() } internal fun stringToChar(value: String): Char { if (value.length != 1) { throw ParseException("$value cannot be parsed to a char") } return value[0] } private inline fun String.tryParse(block: (String) -> T): T { try { return block(this) } catch (cause: DateTimeParseException) { throw ParseException("fail to parse \"$this\" as data time", cause) } } internal fun stringToDate(value: String): Date { return try { Date.from(value.tryParse { Instant.parse(it) }) } catch (e: ParseException) { try { Date.from( value.tryParse { LocalDateTime.parse(it) }.toInstant(ZoneOffset.UTC) ) } catch (e: ParseException) { Date.from( value.tryParse { LocalDate.parse(it) }.atStartOfDay().toInstant(ZoneOffset.UTC) ) } } } private fun ((In) -> Out).asPromote(): PromoteFunc<*> { return { value, _ -> @Suppress("UNCHECKED_CAST") this(value as In) } } private inline fun tryParseAsPromote(noinline block: (String) -> T): PromoteFunc<*> { return { value, _ -> try { block(value as String) } catch (cause: Exception) { if (cause is DateTimeParseException || cause is NumberFormatException) { throw ParseException("fail to parse \"$value\" as ${T::class.simpleName}", cause) } else { throw cause } } } } typealias PromoteFunc = (Any, Source) -> Out private val promoteMap: MutableMap, List, PromoteFunc<*>>>> = mutableMapOf( String::class to listOf( Boolean::class to ::stringToBoolean.asPromote(), Char::class to ::stringToChar.asPromote(), Byte::class to tryParseAsPromote { value: String -> value.toByte() }, Short::class to tryParseAsPromote { value: String -> value.toShort() }, Int::class to tryParseAsPromote { value: String -> value.toInt() }, Long::class to tryParseAsPromote { value: String -> value.toLong() }, Float::class to tryParseAsPromote { value: String -> value.toFloat() }, Double::class to tryParseAsPromote { value: String -> value.toDouble() }, BigInteger::class to tryParseAsPromote { value: String -> value.toBigInteger() }, BigDecimal::class to tryParseAsPromote { value: String -> value.toBigDecimal() }, OffsetTime::class to tryParseAsPromote { OffsetTime.parse(it) }, OffsetDateTime::class to tryParseAsPromote { OffsetDateTime.parse(it) }, ZonedDateTime::class to tryParseAsPromote { ZonedDateTime.parse(it) }, LocalDate::class to tryParseAsPromote { LocalDate.parse(it) }, LocalTime::class to tryParseAsPromote { LocalTime.parse(it) }, LocalDateTime::class to tryParseAsPromote { LocalDateTime.parse(it) }, Year::class to tryParseAsPromote { Year.parse(it) }, YearMonth::class to tryParseAsPromote { YearMonth.parse(it) }, Instant::class to tryParseAsPromote { Instant.parse(it) }, Date::class to ::stringToDate.asPromote(), Duration::class to String::toDuration.asPromote(), SizeInBytes::class to { value: String -> SizeInBytes.parse(value) }.asPromote() ), Char::class to listOf( String::class to { value: Char -> "$value" }.asPromote() ), Byte::class to listOf( Short::class to Byte::toShort.asPromote(), Int::class to Byte::toInt.asPromote(), Long::class to Byte::toLong.asPromote(), Float::class to Byte::toFloat.asPromote(), Double::class to Byte::toDouble.asPromote() ), Short::class to listOf( Byte::class to ::shortToByte.asPromote(), Int::class to Short::toInt.asPromote(), Long::class to Short::toLong.asPromote(), Float::class to Short::toFloat.asPromote(), Double::class to Short::toDouble.asPromote() ), Int::class to listOf( Short::class to ::intToShort.asPromote(), Long::class to Int::toLong.asPromote(), Float::class to Int::toFloat.asPromote(), Double::class to Int::toDouble.asPromote() ), Long::class to listOf( Int::class to ::longToInt.asPromote(), Float::class to Long::toFloat.asPromote(), Double::class to Long::toDouble.asPromote(), BigInteger::class to { value: Long -> BigInteger.valueOf(value) }.asPromote() ), Float::class to listOf( Double::class to Float::toDouble.asPromote() ), Double::class to listOf( Float::class to Double::toFloat.asPromote(), BigDecimal::class to { value: Double -> BigDecimal.valueOf(value) }.asPromote() ) ) private val promoteMatchers: MutableList) -> Boolean, List, PromoteFunc<*>>>>> = mutableListOf( { type: KClass<*> -> type.starProjectedType == Array::class.starProjectedType } to listOf( List::class to { value: Array<*> -> value.asList() }.asPromote(), Set::class to { value: Array<*> -> value.asList().toSet() }.asPromote() ), { type: KClass<*> -> type.isSubclassOf(Set::class) } to listOf( List::class to { value: Set<*> -> value.toList() }.asPromote() ) ) private fun walkPromoteMap( valueType: KClass<*>, targetType: KClass<*>, tasks: Queue<() -> PromoteFunc<*>?>, visitedTypes: MutableSet>, previousPromoteFunc: PromoteFunc<*>? = null ): PromoteFunc<*>? { if (valueType in visitedTypes) { return null } visitedTypes.add(valueType) var promotedTypes = promoteMap[valueType] if (promotedTypes == null) { for ((matcher, types) in promoteMatchers) { if (matcher(valueType)) { promotedTypes = types break } } } if (promotedTypes == null) { return null } for ((promotedType, promoteFunc) in promotedTypes) { val currentPromoteFunc: PromoteFunc<*> = if (previousPromoteFunc != null) { { value, source -> promoteFunc(previousPromoteFunc(value, source)!!, source) } } else { promoteFunc } if (promotedType == targetType) { return currentPromoteFunc } else { tasks.offer { walkPromoteMap(promotedType, targetType, tasks, visitedTypes, currentPromoteFunc) } } } return null } private fun getPromoteFunc(valueType: KClass<*>, targetType: KClass<*>): PromoteFunc<*>? { val tasks = ArrayDeque<() -> PromoteFunc<*>?>() tasks.offer { walkPromoteMap(valueType, targetType, tasks, mutableSetOf()) } while (tasks.isNotEmpty()) { val func = tasks.poll()() if (func != null) { return func } } return null } private fun TreeNode.castOrNull(source: Source, clazz: Class): T? { if (this is ValueNode) { if (clazz.kotlin.javaObjectType.isInstance(value)) { @Suppress("UNCHECKED_CAST") return value as T } else { val promoteFunc = getPromoteFunc(value::class, clazz.kotlin) if (promoteFunc != null) { @Suppress("UNCHECKED_CAST") return promoteFunc(value, source) as T } else { return null } } } else { return null } } private val promotedFromStringTypes = promoteMap.getValue(String::class).map { it.first } private val promotedFromStringMap = promoteMap.getValue(String::class).toMap() private fun TreeNode.toValue(source: Source, type: JavaType, mapper: ObjectMapper): Any { if (this is ValueNode && type == TypeFactory.defaultInstance().constructType(value::class.java) ) { return value } when (type) { is SimpleType -> { val clazz = type.rawClass if (type.isEnumType) { val valueOfMethod = clazz.getMethod("valueOf", String::class.java) val name: String = cast(source) try { return valueOfMethod.invoke(null, name) } catch (cause: InvocationTargetException) { throw ParseException( "enum type $clazz has no constant with name $name", cause ) } } else { val value = castOrNull(source, clazz) if (value != null) { return value } else { try { return mapper.readValue( TreeTraversingParser(withoutPlaceHolder().toJsonNode(source), mapper), type ) } catch (cause: JsonProcessingException) { throw ObjectMappingException("${this.toHierarchical()} in ${source.description}", clazz, cause) } } } } is ArrayType -> { val clazz = type.contentType.rawClass val list = toListValue(source, type.contentType, mapper) if (!clazz.isPrimitive) { val array = java.lang.reflect.Array.newInstance(clazz, list.size) as Array<*> @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") return (list as java.util.Collection<*>).toArray(array) } else { @Suppress("UNCHECKED_CAST") return when (clazz) { Boolean::class.java -> (list as List).toBooleanArray() Int::class.java -> (list as List).toIntArray() Short::class.java -> (list as List).toShortArray() Byte::class.java -> (list as List).toByteArray() Long::class.java -> (list as List).toLongArray() Double::class.java -> (list as List).toDoubleArray() Float::class.java -> (list as List).toFloatArray() Char::class.java -> (list as List).toCharArray() else -> throw UnsupportedTypeException(source, clazz) } } } is CollectionLikeType -> { if (MutableCollection::class.java.isAssignableFrom(type.rawClass)) { @Suppress("UNCHECKED_CAST") return (implOf(type.rawClass).getDeclaredConstructor().newInstance() as MutableCollection).apply { addAll(toListValue(source, type.contentType, mapper) as List) } } else { throw UnsupportedTypeException(source, type.rawClass) } } is MapLikeType -> { if (MutableMap::class.java.isAssignableFrom(type.rawClass)) { when { type.keyType.rawClass == String::class.java -> { @Suppress("UNCHECKED_CAST") return (implOf(type.rawClass).getDeclaredConstructor().newInstance() as MutableMap).apply { putAll( this@toValue.toMap(source).mapValues { (_, value) -> value.toValue(source, type.contentType, mapper) } ) } } type.keyType.rawClass.kotlin in promotedFromStringTypes -> { val promoteFunc = promotedFromStringMap.getValue(type.keyType.rawClass.kotlin) @Suppress("UNCHECKED_CAST") return (implOf(type.rawClass).getDeclaredConstructor().newInstance() as MutableMap).apply { putAll( this@toValue.toMap(source).map { (key, value) -> promoteFunc(key, source)!! to value.toValue(source, type.contentType, mapper) } ) } } else -> { throw UnsupportedMapKeyException(type.keyType.rawClass) } } } else { throw UnsupportedTypeException(source, type.rawClass) } } else -> throw UnsupportedTypeException(source, type.rawClass) } } private fun TreeNode.toListValue(source: Source, type: JavaType, mapper: ObjectMapper): List<*> { return when (this) { is ListNode -> list.map { it.toValue(source, type, mapper) } else -> throw WrongTypeException("$this in ${source.description}", this::class.java.simpleName, List::class.java.simpleName) } } private fun TreeNode.toMap(source: Source): Map { return when (this) { is MapNode -> children else -> throw WrongTypeException("$this in ${source.description}", this::class.java.simpleName, Map::class.java.simpleName) } } private fun TreeNode.toJsonNode(source: Source): JsonNode { return when (this) { is NullNode -> JacksonNullNode.instance is ListStringNode -> ArrayNode( JsonNodeFactory.instance, list.map { it.toJsonNode(source) } ) is ValueNode -> { when (value) { is Boolean -> BooleanNode.valueOf(value as Boolean) is Long -> LongNode.valueOf(value as Long) is Int -> IntNode.valueOf(value as Int) is Short -> ShortNode.valueOf(value as Short) is Byte -> ShortNode.valueOf((value as Byte).toShort()) is BigInteger -> BigIntegerNode.valueOf(value as BigInteger) is Double -> DoubleNode.valueOf(value as Double) is Float -> FloatNode.valueOf(value as Float) is Char -> TextNode.valueOf(value.toString()) is BigDecimal -> DecimalNode.valueOf(value as BigDecimal) is String -> TextNode.valueOf(value as String) is OffsetTime -> TextNode.valueOf(value.toString()) is OffsetDateTime -> TextNode.valueOf(value.toString()) is ZonedDateTime -> TextNode.valueOf(value.toString()) is LocalDate -> TextNode.valueOf(value.toString()) is LocalTime -> TextNode.valueOf(value.toString()) is LocalDateTime -> TextNode.valueOf(value.toString()) is Date -> TextNode.valueOf((value as Date).toInstant().toString()) is Year -> TextNode.valueOf(value.toString()) is YearMonth -> TextNode.valueOf(value.toString()) is Instant -> TextNode.valueOf(value.toString()) is Duration -> TextNode.valueOf(value.toString()) is SizeInBytes -> LongNode.valueOf((value as SizeInBytes).bytes) else -> throw ParseException("fail to cast source ${source.description} to JSON node") } } is ListNode -> ArrayNode( JsonNodeFactory.instance, list.map { it.toJsonNode(source) } ) is MapNode -> ObjectNode( JsonNodeFactory.instance, children.mapValues { (_, value) -> value.toJsonNode(source) } ) else -> throw ParseException("fail to cast source ${source.description} to JSON node") } } private fun implOf(clazz: Class<*>): Class<*> = when (clazz) { List::class.java -> ArrayList::class.java Set::class.java -> HashSet::class.java SortedSet::class.java -> TreeSet::class.java Map::class.java -> HashMap::class.java SortedMap::class.java -> TreeMap::class.java else -> clazz } fun Any.asTree(): TreeNode = when (this) { is TreeNode -> this is Source -> this.tree is List<*> -> @Suppress("UNCHECKED_CAST") ListSourceNode((this as List).map { it.asTree() }) is Map<*, *> -> { when { this.size == 0 -> ContainerNode(mutableMapOf()) this.iterator().next().key is String -> { @Suppress("UNCHECKED_CAST") ContainerNode( (this as Map).mapValues { (_, value) -> value.asTree() }.toMutableMap() ) } this.iterator().next().key!!::class in listOf( Char::class, Byte::class, Short::class, Int::class, Long::class, BigInteger::class ) -> { @Suppress("UNCHECKED_CAST") ContainerNode( (this as Map).map { (key, value) -> key.toString() to value.asTree() }.toMap().toMutableMap() ) } else -> ValueSourceNode(this) } } else -> ValueSourceNode(this) } fun Any.asSource(type: String = "", info: SourceInfo = SourceInfo()): Source = when (this) { is Source -> this is TreeNode -> Source(info.with("type" to type), this) else -> Source(info.with("type" to type), asTree()) } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceException.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.ConfigException import com.uchuhimo.konf.Path import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.name /** * Exception for source. */ open class SourceException : ConfigException { constructor(message: String) : super(message) constructor(message: String, cause: Throwable) : super(message, cause) } /** * Exception indicates that actual type of value in source is unmatched with expected type. */ class WrongTypeException(val source: String, actual: String, expected: String) : SourceException("source $source has type $actual rather than $expected") /** * Exception indicates that expected value in specified path is not existed in the source. */ class NoSuchPathException(val source: Source, val path: Path) : SourceException("cannot find path \"${path.name}\" in source ${source.description}") /** * Exception indicates that there is a parsing error. */ class ParseException : SourceException { constructor(message: String) : super(message) constructor(message: String, cause: Throwable) : super(message, cause) } /** * Exception indicates that value of specified class in unsupported in the source. */ class UnsupportedTypeException(source: Source, clazz: Class<*>) : SourceException("value of type ${clazz.simpleName} is unsupported in source ${source.description}") /** * Exception indicates that watch key is no longer valid for the source. */ class InvalidWatchKeyException(source: Source) : SourceException("watch key for source ${source.description} is no longer valid") /** * Exception indicates that the given repository is not in the remote list of the local repository. */ class InvalidRemoteRepoException(repo: String, dir: String) : SourceException("$repo is not in the remote list of $dir") /** * Exception indicates failure to map source to value of specified class. */ class ObjectMappingException(source: String, clazz: Class<*>, cause: Throwable) : SourceException("unable to map source $source to value of type ${clazz.simpleName}", cause) /** * Exception indicates that value of specified class is unsupported as key of map. */ class UnsupportedMapKeyException(val clazz: Class<*>) : SourceException( "cannot support map with ${clazz.simpleName} key" ) /** * Exception indicates failure to load specified path. */ class LoadException(val path: Path, cause: Throwable) : SourceException("fail to load ${path.name}", cause) /** * Exception indicates that the source contains unknown paths. */ class UnknownPathsException(source: Source, val paths: List) : SourceException( "source ${source.description} contains the following unknown paths:\n" + paths.joinToString("\n") ) /** * Exception indicates that specified source is not found. */ class SourceNotFoundException(message: String) : SourceException(message) /** * Exception indicates that specified source has unsupported extension. */ class UnsupportedExtensionException(source: String) : SourceException( "cannot detect supported extension for \"$source\"," + " supported extensions: conf, json, properties, toml, xml, yml, yaml" ) /** * Exception indicates that undefined paths occur during variable substitution. */ class UndefinedPathVariableException(val source: Source, val text: String) : SourceException( "\"$text\" in source ${source.description} contains undefined path variables during path substitution" ) /** * Exception indicates that the specified node has unsupported type. */ class UnsupportedNodeTypeException(val source: Source, val node: TreeNode) : SourceException( "$node of type ${node::class.java.simpleName} in source ${source.description} is unsupported" ) ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceNode.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.ListNode import com.uchuhimo.konf.MapNode import com.uchuhimo.konf.NullNode import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.emptyMutableMap import java.util.Collections interface SubstitutableNode : ValueNode { fun substitute(value: String): TreeNode val substituted: Boolean val originalValue: Any? } class ValueSourceNode( override val value: Any, override val substituted: Boolean = false, override val originalValue: Any? = null ) : SubstitutableNode { override fun substitute(value: String): TreeNode { return ValueSourceNode(value, true, originalValue ?: this.value) } } object NullSourceNode : NullNode { override val children: MutableMap = emptyMutableMap } open class ListSourceNode( override val list: List, override var isPlaceHolder: Boolean = false ) : ListNode, MapNode { override val children: MutableMap get() = Collections.unmodifiableMap( list.withIndex().associate { (key, value) -> key.toString() to value } ) override fun withList(list: List): ListNode { return ListSourceNode(list) } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/Utils.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.getUnits import java.time.Duration import java.time.format.DateTimeParseException import java.util.concurrent.TimeUnit /** * Parses specified string to duration. * * @receiver specified string * @return duration */ fun String.toDuration(): Duration { return try { Duration.parse(this) } catch (e: DateTimeParseException) { Duration.ofNanos(parseDuration(this)) } } /** * Parses a duration string. If no units are specified in the string, it is * assumed to be in milliseconds. The returned duration is in nanoseconds. * * @param input the string to parse * @return duration in nanoseconds */ internal fun parseDuration(input: String): Long { val s = input.trim() val originalUnitString = getUnits(s) var unitString = originalUnitString val numberString = s.substring(0, s.length - unitString.length).trim() // this would be caught later anyway, but the error message // is more helpful if we check it here. if (numberString.isEmpty()) throw ParseException("No number in duration value '$input'") if (unitString.length > 2 && !unitString.endsWith("s")) unitString += "s" // note that this is deliberately case-sensitive val units = if (unitString == "" || unitString == "ms" || unitString == "millis" || unitString == "milliseconds" ) { TimeUnit.MILLISECONDS } else if (unitString == "us" || unitString == "micros" || unitString == "microseconds") { TimeUnit.MICROSECONDS } else if (unitString == "ns" || unitString == "nanos" || unitString == "nanoseconds") { TimeUnit.NANOSECONDS } else if (unitString == "d" || unitString == "days") { TimeUnit.DAYS } else if (unitString == "h" || unitString == "hours") { TimeUnit.HOURS } else if (unitString == "s" || unitString == "seconds") { TimeUnit.SECONDS } else if (unitString == "m" || unitString == "minutes") { TimeUnit.MINUTES } else { throw ParseException("Could not parse time unit '$originalUnitString' (try ns, us, ms, s, m, h, d)") } return try { // if the string is purely digits, parse as an integer to avoid // possible precision loss; // otherwise as a double. if (numberString.matches("[+-]?[0-9]+".toRegex())) { units.toNanos(java.lang.Long.parseLong(numberString)) } else { val nanosInUnit = units.toNanos(1) (java.lang.Double.parseDouble(numberString) * nanosInUnit).toLong() } } catch (e: NumberFormatException) { throw ParseException("Could not parse duration number '$numberString'") } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/Writer.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import java.io.File import java.io.OutputStream import java.io.StringWriter /** * Save config to various output format. */ interface Writer { /** * Save to specified writer. * * @param writer specified writer for writing character streams */ fun toWriter(writer: java.io.Writer) /** * Save to specified output stream. * * @param outputStream specified output stream of bytes */ fun toOutputStream(outputStream: OutputStream) /** * Save to specified file. * * @param file specified file * @return a new source from specified file */ fun toFile(file: File) { file.outputStream().use { toOutputStream(it) } } /** * Save to specified file path. * * @param file specified file path */ fun toFile(file: String) = toFile(File(file)) /** * Save to string. * * @return string */ fun toText(): String = StringWriter().apply { toWriter(this) }.toString() /** * Save to byte array. * * @return byte array */ fun toBytes(): ByteArray = toText().toByteArray() } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/FlatSource.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.uchuhimo.konf.Config import com.uchuhimo.konf.ContainerNode import com.uchuhimo.konf.ListNode import com.uchuhimo.konf.PathConflictException import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.notEmptyOr import com.uchuhimo.konf.source.ListSourceNode import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.SourceInfo import com.uchuhimo.konf.source.SubstitutableNode import com.uchuhimo.konf.source.ValueSourceNode import com.uchuhimo.konf.source.asTree import java.util.Collections /** * Source from a map in flat format. */ open class FlatSource( val map: Map, type: String = "", final override val info: SourceInfo = SourceInfo(), private val allowConflict: Boolean = false ) : Source { init { info["type"] = type.notEmptyOr("flat") } override val tree: TreeNode = ContainerNode(mutableMapOf()).apply { map.forEach { (path, value) -> try { set(path, value.asTree()) } catch (ex: PathConflictException) { if (!allowConflict) { throw ex } } } }.promoteToList() } object EmptyStringNode : SubstitutableNode, ListNode { override val value: Any = "" override val list: List = listOf() override val originalValue: Any? = null override val substituted: Boolean = false override fun substitute(value: String): TreeNode { check(value.isEmpty()) return this } } class SingleStringListNode( override val value: String, override val substituted: Boolean = false, override val originalValue: Any? = null ) : SubstitutableNode, ListNode { override val children: MutableMap = Collections.unmodifiableMap( mutableMapOf("0" to value.asTree()) ) override val list: List = listOf(value.asTree()) override fun substitute(value: String): TreeNode = value.promoteToList( true, originalValue ?: this.value ) } class ListStringNode( override val value: String, override val substituted: Boolean = false, override val originalValue: Any? = null ) : ListSourceNode(value.split(',').map { ValueSourceNode(it) }), SubstitutableNode { override fun substitute(value: String): TreeNode = value.promoteToList(true, originalValue ?: this.value) override val children: MutableMap get() = super.children } fun String.promoteToList(substitute: Boolean = false, originalValue: Any? = null): TreeNode { return when { ',' in this -> ListStringNode(this, substitute, originalValue) this == "" -> EmptyStringNode else -> SingleStringListNode(this, substitute, originalValue) } } fun ContainerNode.promoteToList(): TreeNode { for ((key, child) in children) { if (child is ContainerNode) { children[key] = child.promoteToList() } else if (child is ValueNode) { val value = child.value if (value is String) { children[key] = value.promoteToList() } } } val list = generateSequence(0) { it + 1 }.map { val key = it.toString() if (key in children) key else null }.takeWhile { it != null }.filterNotNull().toList() if (list.isNotEmpty() && list.toSet() == children.keys) { return ListSourceNode(list.map { children[it]!! }) } else { return this } } /** * Returns a map in flat format for this config. * * The returned map contains all items in this config. * This map can be loaded into config as [com.uchuhimo.konf.source.base.FlatSource] using * `config.from.map.flat(map)`. */ fun Config.toFlatMap(): Map { fun MutableMap.putFlat(key: String, value: Any) { when (value) { is List<*> -> { if (value.isNotEmpty()) { val first = value[0] when (first) { is List<*>, is Map<*, *> -> value.forEachIndexed { index, child -> putFlat("$key.$index", child!!) } else -> { if (value.map { it.toString() }.any { it.contains(',') }) { value.forEachIndexed { index, child -> putFlat("$key.$index", child!!) } } else { put(key, value.joinToString(",")) } } } } else { put(key, "") } } is Map<*, *> -> value.forEach { (suffix, child) -> putFlat("$key.$suffix", child!!) } else -> put(key, value.toString()) } } return mutableMapOf().apply { for ((key, value) in this@toFlatMap.toMap()) { putFlat(key, value) } } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/KVSource.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.uchuhimo.konf.ContainerNode import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.notEmptyOr import com.uchuhimo.konf.source.SourceInfo import com.uchuhimo.konf.source.asTree /** * Source from a map in key-value format. */ open class KVSource( val map: Map, type: String = "", info: SourceInfo = SourceInfo() ) : ValueSource(map, type.notEmptyOr("KV"), info) { override val tree: TreeNode = map.kvToTree() } fun Map.kvToTree(): TreeNode { return ContainerNode(mutableMapOf()).apply { this@kvToTree.forEach { (path, value) -> set(path, value.asTree()) } } } fun Map.asKVSource() = KVSource(this) ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/MapSource.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.uchuhimo.konf.Config import com.uchuhimo.konf.ListNode import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.notEmptyOr import com.uchuhimo.konf.source.SourceInfo import com.uchuhimo.konf.toTree /** * Source from a hierarchical map. */ open class MapSource( val map: Map, type: String = "", info: SourceInfo = SourceInfo() ) : ValueSource(map, type.notEmptyOr("map"), info) /** * Returns a hierarchical map for this config. * * The returned map contains all items in this config. * This map can be loaded into config as [com.uchuhimo.konf.source.base.MapSource] using * `config.from.map.hierarchical(map)`. */ @Suppress("UNCHECKED_CAST") fun Config.toHierarchicalMap(): Map { return toTree().toHierarchical() as Map } /** * Returns a hierarchical value for this tree node. * * The returned value contains all items in this tree node. */ fun TreeNode.toHierarchical(): Any = withoutPlaceHolder().toHierarchicalInternal() private fun TreeNode.toHierarchicalInternal(): Any { when (this) { is ValueNode -> return value is ListNode -> return list.map { it.toHierarchicalInternal() } else -> return children.mapValues { (_, child) -> child.toHierarchicalInternal() } } } /** * Source from an empty map. */ class EmptyMapSource : MapSource(emptyMap(), "empty map") ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/ValueSource.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.notEmptyOr import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.SourceInfo import com.uchuhimo.konf.source.asTree /** * Source from a single value. */ open class ValueSource( val value: Any, type: String = "", final override val info: SourceInfo = SourceInfo() ) : Source { init { info["type"] = type.notEmptyOr("value") } override val tree: TreeNode = value.asTree() } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/deserializer/DurationDeserializer.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import com.uchuhimo.konf.source.SourceException import com.uchuhimo.konf.source.toDuration import java.time.Duration import java.time.format.DateTimeParseException /** * Deserializer for [Duration]. */ object DurationDeserializer : JSR310Deserializer(Duration::class.java) { override fun parse(string: String): Duration { return try { Duration.parse(string) } catch (exception: DateTimeParseException) { try { string.toDuration() } catch (_: SourceException) { throw exception } } } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/deserializer/EmptyStringToCollectionDeserializerModifier.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.databind.BeanDescription import com.fasterxml.jackson.databind.BeanProperty import com.fasterxml.jackson.databind.DeserializationConfig import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier import com.fasterxml.jackson.databind.deser.ContextualDeserializer import com.fasterxml.jackson.databind.deser.ResolvableDeserializer import com.fasterxml.jackson.databind.type.ArrayType import com.fasterxml.jackson.databind.type.CollectionType import com.fasterxml.jackson.databind.type.MapType object EmptyStringToCollectionDeserializerModifier : BeanDeserializerModifier() { override fun modifyMapDeserializer( config: DeserializationConfig?, type: MapType?, beanDesc: BeanDescription?, deserializer: JsonDeserializer<*> ): JsonDeserializer<*>? = object : JsonDeserializer>(), ContextualDeserializer, ResolvableDeserializer { @Suppress("UNCHECKED_CAST") override fun deserialize(jp: JsonParser, ctx: DeserializationContext?): Map? { if (!jp.isExpectedStartArrayToken && jp.hasToken(JsonToken.VALUE_STRING) && jp.text.isEmpty()) { return deserializer.getEmptyValue(ctx) as Map? } return deserializer.deserialize(jp, ctx) as Map? } override fun createContextual( ctx: DeserializationContext?, property: BeanProperty? ): JsonDeserializer<*>? = modifyMapDeserializer( config, type, beanDesc, (deserializer as ContextualDeserializer) .createContextual(ctx, property) ) override fun resolve(ctx: DeserializationContext?) { (deserializer as? ResolvableDeserializer)?.resolve(ctx) } } override fun modifyCollectionDeserializer( config: DeserializationConfig?, type: CollectionType?, beanDesc: BeanDescription?, deserializer: JsonDeserializer<*> ): JsonDeserializer<*>? = object : JsonDeserializer>(), ContextualDeserializer { @Suppress("UNCHECKED_CAST") override fun deserialize(jp: JsonParser, ctx: DeserializationContext?): Collection? { if (!jp.isExpectedStartArrayToken && jp.hasToken(JsonToken.VALUE_STRING) && jp.text.isEmpty()) { return deserializer.getEmptyValue(ctx) as Collection? } return deserializer.deserialize(jp, ctx) as Collection? } override fun createContextual( ctx: DeserializationContext?, property: BeanProperty? ): JsonDeserializer<*>? = modifyCollectionDeserializer( config, type, beanDesc, (deserializer as ContextualDeserializer) .createContextual(ctx, property) ) } override fun modifyArrayDeserializer( config: DeserializationConfig?, valueType: ArrayType?, beanDesc: BeanDescription?, deserializer: JsonDeserializer<*> ): JsonDeserializer<*> = object : JsonDeserializer(), ContextualDeserializer { @Suppress("UNCHECKED_CAST") override fun deserialize(jp: JsonParser, ctx: DeserializationContext?): Any? { if (!jp.isExpectedStartArrayToken && jp.hasToken(JsonToken.VALUE_STRING) && jp.text.isEmpty()) { val emptyValue = deserializer.getEmptyValue(ctx) return if (emptyValue is Array<*>) { java.lang.reflect.Array.newInstance(valueType!!.contentType.rawClass, 0) } else { emptyValue } } return deserializer.deserialize(jp, ctx) } override fun createContextual( ctx: DeserializationContext?, property: BeanProperty? ): JsonDeserializer<*>? = modifyArrayDeserializer( config, valueType, beanDesc, (deserializer as ContextualDeserializer) .createContextual(ctx, property) ) } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/deserializer/JSR310Deserializer.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonTokenId import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.exc.MismatchedInputException import java.time.format.DateTimeParseException /** * Base class of deserializers for datetime classes in JSR310. * * @param T type of datetime value */ abstract class JSR310Deserializer(clazz: Class) : StdDeserializer(clazz) { /** * Parses from a string to datetime value. * * @param string input string * @return datetime value * @throws DateTimeParseException */ abstract fun parse(string: String): T final override fun deserialize(parser: JsonParser, context: DeserializationContext): T? { when (parser.currentTokenId()) { JsonTokenId.ID_STRING -> { val string = parser.text.trim { it <= ' ' } if (string.isEmpty()) { return null } try { return parse(string) } catch (exception: DateTimeParseException) { throw context.weirdStringException(string, handledType(), exception.message).apply { initCause(exception) } } } } throw MismatchedInputException.from( parser, handledType(), "Unexpected token (${parser.currentToken}), expected string for ${handledType().name} value" ) } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/deserializer/OffsetDateTimeDeserializer.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import java.time.OffsetDateTime /** * Deserializer for [OffsetDateTime]. */ object OffsetDateTimeDeserializer : JSR310Deserializer(OffsetDateTime::class.java) { override fun parse(string: String): OffsetDateTime = OffsetDateTime.parse(string) } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/deserializer/StringDeserializer.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.deser.std.StringDeserializer as JacksonStringDeserializer object StringDeserializer : JacksonStringDeserializer() { override fun _deserializeFromArray(p: JsonParser, ctxt: DeserializationContext): String? { val t = p.nextToken() if (t == JsonToken.END_ARRAY && ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { return getNullValue(ctxt) } if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { val parsed = deserialize(p, ctxt) val token = p.nextToken() if (token != JsonToken.END_ARRAY) { return parsed + "," + deserializeFromRestOfArray(token, p, ctxt) } return parsed } return deserializeFromRestOfArray(t, p, ctxt) } private fun deserializeFromRestOfArray( token: JsonToken, p: JsonParser, ctxt: DeserializationContext ): String { var t = token val sb = StringBuilder(64) while (t != JsonToken.END_ARRAY) { val str = if (t == JsonToken.VALUE_STRING) { p.text } else { _parseString(p, ctxt) } if (sb.isEmpty()) { sb.append(str) } else { sb.append(',').append(str) } t = p.nextToken() } return sb.toString() } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/deserializer/ZoneDateTimeDeserializer.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import java.time.ZonedDateTime /** * Deserializer for [ZonedDateTime]. */ object ZoneDateTimeDeserializer : JSR310Deserializer(ZonedDateTime::class.java) { override fun parse(string: String): ZonedDateTime = ZonedDateTime.parse(string) } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/env/EnvProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.env import com.uchuhimo.konf.Feature import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.base.FlatSource /** * Provider for system environment source. */ object EnvProvider { private val validEnv = Regex("(\\w+)(.\\w+)*") /** * Returns a new source from system environment. * * @param nested whether to treat "AA_BB_CC" as nested format "AA.BB.CC" or not. True by default. * @return a new source from system environment */ @JvmOverloads fun env(nested: Boolean = true): Source = envMap(System.getenv(), nested) @JvmOverloads fun envMap(map: Map, nested: Boolean = true): Source { return FlatSource( map.mapKeys { (key, _) -> if (nested) key.replace('_', '.') else key }.filter { (key, _) -> key.matches(validEnv) }.toSortedMap(), type = "system-environment", allowConflict = true ).enabled( Feature.LOAD_KEYS_CASE_INSENSITIVELY ).disabled( Feature.SUBSTITUTE_SOURCE_BEFORE_LOADED ) } @JavaApi @JvmStatic fun get() = this } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/json/JsonProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.json import com.fasterxml.jackson.databind.ObjectMapper import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.Provider import com.uchuhimo.konf.source.Source import java.io.InputStream import java.io.Reader /** * Provider for JSON source. */ object JsonProvider : Provider { override fun reader(reader: Reader): Source = JsonSource(ObjectMapper().readTree(reader)) override fun inputStream(inputStream: InputStream): Source = JsonSource(ObjectMapper().readTree(inputStream)) @JavaApi @JvmStatic fun get() = this } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/json/JsonSource.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.json import com.fasterxml.jackson.databind.JsonNode import com.uchuhimo.konf.ContainerNode import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.source.ListSourceNode import com.uchuhimo.konf.source.NullSourceNode import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.SourceInfo import com.uchuhimo.konf.source.ValueSourceNode /** * Source from a JSON node. */ class JsonSource( val node: JsonNode ) : Source { override val info: SourceInfo = SourceInfo("type" to "JSON") override val tree: TreeNode = node.toTree() } fun JsonNode.toTree(): TreeNode { return when { isNull -> NullSourceNode isBoolean -> ValueSourceNode(booleanValue()) isNumber -> ValueSourceNode(numberValue()) isTextual -> ValueSourceNode(textValue()) isArray -> ListSourceNode( mutableListOf().apply { elements().forEach { add(it.toTree()) } } ) isObject -> ContainerNode( mutableMapOf().apply { for ((key, value) in fields()) { put(key, value.toTree()) } } ) isMissingNode -> ContainerNode(mutableMapOf()) else -> throw NotImplementedError() } } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/json/JsonWriter.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.json import com.fasterxml.jackson.core.util.DefaultIndenter import com.fasterxml.jackson.core.util.DefaultPrettyPrinter import com.fasterxml.jackson.databind.ObjectWriter import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.Writer import com.uchuhimo.konf.source.base.toHierarchicalMap import java.io.OutputStream /** * Writer for JSON source. */ class JsonWriter(val config: Config) : Writer { private val objectWriter: ObjectWriter = config.mapper.writer( DefaultPrettyPrinter().withObjectIndenter( DefaultIndenter().withLinefeed(System.lineSeparator()) ) ) override fun toWriter(writer: java.io.Writer) { objectWriter.writeValue(writer, config.toHierarchicalMap()) } override fun toOutputStream(outputStream: OutputStream) { objectWriter.writeValue(outputStream, config.toHierarchicalMap()) } } /** * Returns Writer for JSON source. */ val Config.toJson: Writer get() = JsonWriter(this) ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/properties/PropertiesProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.properties import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.Provider import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.base.FlatSource import java.io.InputStream import java.io.Reader import java.util.Properties /** * Provider for properties source. */ object PropertiesProvider : Provider { @Suppress("UNCHECKED_CAST") private fun Properties.toMap(): Map = this as Map override fun reader(reader: Reader): Source = FlatSource(Properties().apply { load(reader) }.toMap(), type = "properties") override fun inputStream(inputStream: InputStream): Source = FlatSource(Properties().apply { load(inputStream) }.toMap(), type = "properties") /** * Returns a new source from system properties. * * @return a new source from system properties */ fun system(): Source = FlatSource( System.getProperties().toMap(), type = "system-properties", allowConflict = true ) @JavaApi @JvmStatic fun get() = this } ================================================ FILE: konf-core/src/main/kotlin/com/uchuhimo/konf/source/properties/PropertiesWriter.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.properties import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.Writer import com.uchuhimo.konf.source.base.toFlatMap import java.io.FilterOutputStream import java.io.OutputStream import java.util.Properties /** * Writer for properties source. */ class PropertiesWriter(val config: Config) : Writer { override fun toWriter(writer: java.io.Writer) { NoCommentProperties().apply { putAll(config.toFlatMap()) }.store(writer, null) } override fun toOutputStream(outputStream: OutputStream) { NoCommentProperties().apply { putAll(config.toFlatMap()) }.store(outputStream, null) } } private class NoCommentProperties : Properties() { private class StripFirstLineStream(out: OutputStream) : FilterOutputStream(out) { private var firstLineSeen = false override fun write(b: Int) { if (firstLineSeen) { super.write(b) } else if (b == '\n'.toInt()) { firstLineSeen = true } } } private class StripFirstLineWriter(writer: java.io.Writer) : java.io.FilterWriter(writer) { override fun write(cbuf: CharArray, off: Int, len: Int) { val offset = cbuf.indexOfFirst { it == '\n' } super.write(cbuf, offset + 1, len - offset - 1) } } override fun store(out: OutputStream, comments: String?) { super.store(StripFirstLineStream(out), null) } override fun store(writer: java.io.Writer, comments: String?) { super.store(StripFirstLineWriter(writer), null) } } /** * Returns writer for properties source. */ val Config.toProperties: Writer get() = PropertiesWriter(this) ================================================ FILE: konf-core/src/test/java/com/uchuhimo/konf/AnonymousConfigSpec.java ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf; public class AnonymousConfigSpec { public static Spec spec = new ConfigSpec() {}; } ================================================ FILE: konf-core/src/test/java/com/uchuhimo/konf/ConfigJavaApiTest.java ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import com.uchuhimo.konf.source.Source; import java.util.HashMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName("test Java API of Config") class ConfigJavaApiTest { private Config config; @BeforeEach void initConfig() { config = Configs.create(); config.addSpec(NetworkBufferInJava.spec); } @Test @DisplayName("test `Configs.create`") void create() { final Config config = Configs.create(); assertThat(config.getItems().size(), equalTo(0)); } @Test @DisplayName("test `Configs.create` with init block") void createWithInit() { final Config config = Configs.create(it -> it.addSpec(NetworkBufferInJava.spec)); assertThat(config.getItems().size(), equalTo(5)); } @Test @DisplayName("test fluent API to load from map") void loadFromMap() { final HashMap map = new HashMap<>(); map.put(config.nameOf(NetworkBufferInJava.size), 1024); final Config newConfig = config.from().map.kv(map); assertThat(newConfig.get(NetworkBufferInJava.size), equalTo(1024)); } @Test @DisplayName("test fluent API to load from system properties") void loadFromSystem() { System.setProperty(config.nameOf(NetworkBufferInJava.size), "1024"); final Config newConfig = config.from().systemProperties(); assertThat(newConfig.get(NetworkBufferInJava.size), equalTo(1024)); } @Test @DisplayName("test fluent API to load from source") void loadFromSource() { final HashMap map = new HashMap<>(); map.put(config.nameOf(NetworkBufferInJava.size), 1024); final Config newConfig = config.withSource(Source.from().map.kv(map)); assertThat(newConfig.get(NetworkBufferInJava.size), equalTo(1024)); } @Test @DisplayName("test `get(Item)`") void getWithItem() { final String name = config.get(NetworkBufferInJava.name); assertThat(name, equalTo("buffer")); } @Test @DisplayName("test `get(String)`") void getWithName() { final NetworkBuffer.Type type = config.get(config.nameOf(NetworkBufferInJava.type)); assertThat(type, equalTo(NetworkBuffer.Type.OFF_HEAP)); } @Test @DisplayName("test `set(Item, T)`") void setWithItem() { config.set(NetworkBufferInJava.size, 1024); assertThat(config.get(NetworkBufferInJava.size), equalTo(1024)); } @Test @DisplayName("test `set(String, T)`") void setWithName() { config.set(config.nameOf(NetworkBufferInJava.size), 1024); assertThat(config.get(NetworkBufferInJava.size), equalTo(1024)); } @Test @DisplayName("test `lazySet(Item, Function1)`") void lazySetWithItem() { config.lazySet(NetworkBufferInJava.maxSize, it -> it.get(NetworkBufferInJava.size) * 4); config.set(NetworkBufferInJava.size, 1024); assertThat(config.get(NetworkBufferInJava.maxSize), equalTo(1024 * 4)); } @Test @DisplayName("test `lazySet(String, Function1)`") void lazySetWithName() { config.lazySet( config.nameOf(NetworkBufferInJava.maxSize), it -> it.get(NetworkBufferInJava.size) * 4); config.set(NetworkBufferInJava.size, 1024); assertThat(config.get(NetworkBufferInJava.maxSize), equalTo(1024 * 4)); } } ================================================ FILE: konf-core/src/test/java/com/uchuhimo/konf/NetworkBufferInJava.java ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf; public class NetworkBufferInJava { public static final ConfigSpec spec = new ConfigSpec("network.buffer"); public static final RequiredItem size = new RequiredItem(spec, "size", "size of buffer in KB") {}; public static final LazyItem maxSize = new LazyItem( spec, "maxSize", config -> config.get(size) * 2, "max size of buffer in KB") {}; public static final OptionalItem name = new OptionalItem(spec, "name", "buffer", "name of buffer") {}; public static final OptionalItem type = new OptionalItem( spec, "type", NetworkBuffer.Type.OFF_HEAP, "type of network buffer.\n" + "two type:\n" + "- on-heap\n" + "- off-heap\n" + "buffer is off-heap by default.") {}; public static final OptionalItem offset = new OptionalItem(spec, "offset", null, "initial offset of buffer", null, true) {}; } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/AdHocConfigItemSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.toValue import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import kotlin.test.assertNull object AdHocConfigItemSpec : Spek({ on("load config into ad-hoc config class with ad-hoc config items") { val config = Config().from.map.kv( mapOf( "network.buffer.size" to 1, "network.buffer.heap.type" to AdHocNetworkBuffer.Type.ON_HEAP, "network.buffer.offset" to 0 ) ) val networkBuffer = AdHocNetworkBuffer(config) it("should load correct values") { assertThat(networkBuffer.size, equalTo(1)) assertThat(networkBuffer.maxSize, equalTo(2)) assertThat(networkBuffer.name, equalTo("buffer")) assertThat(networkBuffer.type, equalTo(AdHocNetworkBuffer.Type.ON_HEAP)) assertThat(networkBuffer.offset, equalTo(0)) } } val source = Source.from.map.hierarchical( mapOf( "size" to 1, "maxSize" to 2, "name" to "buffer", "type" to "ON_HEAP", "offset" to "null" ) ) on("cast config to config class property") { val networkBufferForCast: NetworkBufferForCast by Config().withSource(source).cast() it("should load correct values") { assertThat(networkBufferForCast.size, equalTo(1)) assertThat(networkBufferForCast.maxSize, equalTo(2)) assertThat(networkBufferForCast.name, equalTo("buffer")) assertThat(networkBufferForCast.type, equalTo(NetworkBufferForCast.Type.ON_HEAP)) assertNull(networkBufferForCast.offset) } } on("cast config to config class") { val networkBufferForCast = Config().withSource(source).toValue() it("should load correct values") { assertThat(networkBufferForCast.size, equalTo(1)) assertThat(networkBufferForCast.maxSize, equalTo(2)) assertThat(networkBufferForCast.name, equalTo("buffer")) assertThat(networkBufferForCast.type, equalTo(NetworkBufferForCast.Type.ON_HEAP)) assertNull(networkBufferForCast.offset) } } on("cast multi-layer config to config class") { val networkBufferForCast = Config().withSource(source).from.json.string("").toValue() it("should load correct values") { assertThat(networkBufferForCast.size, equalTo(1)) assertThat(networkBufferForCast.maxSize, equalTo(2)) assertThat(networkBufferForCast.name, equalTo("buffer")) assertThat(networkBufferForCast.type, equalTo(NetworkBufferForCast.Type.ON_HEAP)) assertNull(networkBufferForCast.offset) } } on("cast config with merged source to config class") { val networkBufferForCast = Config().withSource(source + Source.from.json.string("")).toValue() it("should load correct values") { assertThat(networkBufferForCast.size, equalTo(1)) assertThat(networkBufferForCast.maxSize, equalTo(2)) assertThat(networkBufferForCast.name, equalTo("buffer")) assertThat(networkBufferForCast.type, equalTo(NetworkBufferForCast.Type.ON_HEAP)) assertNull(networkBufferForCast.offset) } } on("cast source to config class") { val networkBufferForCast = source.toValue() it("should load correct values") { assertThat(networkBufferForCast.size, equalTo(1)) assertThat(networkBufferForCast.maxSize, equalTo(2)) assertThat(networkBufferForCast.name, equalTo("buffer")) assertThat(networkBufferForCast.type, equalTo(NetworkBufferForCast.Type.ON_HEAP)) assertNull(networkBufferForCast.offset) } } }) data class NetworkBufferForCast( val size: Int, val maxSize: Int, val name: String, val type: Type, val offset: Int? ) { enum class Type { ON_HEAP, OFF_HEAP } } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/AdHocNetworkBuffer.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf class AdHocNetworkBuffer(config: Config) { private val root = config.at("network.buffer") val size: Int by root.required(description = "size of buffer in KB") val maxSize by root.lazy(name = "max-size", description = "max size of buffer in KB") { size * 2 } val name by root.optional("buffer", description = "name of buffer") val type by root.optional( Type.OFF_HEAP, prefix = "heap", description = """ | type of network buffer. | two type: | - on-heap | - off-heap | buffer is off-heap by default. """.trimMargin("| ") ) val offset by root.optional(null, description = "initial offset of buffer") enum class Type { ON_HEAP, OFF_HEAP } } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/ConfigInJavaSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.absent import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.has import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.base.asKVSource import com.uchuhimo.konf.source.base.toHierarchicalMap import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue object ConfigInJavaSpec : SubjectSpek({ val spec = NetworkBufferInJava.spec val size = NetworkBufferInJava.size val maxSize = NetworkBufferInJava.maxSize val name = NetworkBufferInJava.name val type = NetworkBufferInJava.type val offset = NetworkBufferInJava.offset val prefix = "network.buffer" fun qualify(name: String): String = "$prefix.$name" subject { Config { addSpec(spec) } } given("a config") { val invalidItem by ConfigSpec("invalid").required() val invalidItemName = "invalid.invalidItem" group("addSpec operation") { on("add orthogonal spec") { val newSpec = object : ConfigSpec(spec.prefix) { val minSize by optional(1) } val config = subject.withSource(mapOf(newSpec.qualify(newSpec.minSize) to 2).asKVSource()) config.addSpec(newSpec) it("should contain items in new spec") { assertTrue { newSpec.minSize in config } assertTrue { spec.qualify(newSpec.minSize) in config } assertThat(config.nameOf(newSpec.minSize), equalTo(spec.qualify(newSpec.minSize))) } it("should contain new spec") { assertThat(newSpec in config.specs, equalTo(true)) assertThat(spec in config.specs, equalTo(true)) } it("should load values from the existed sources for items in new spec") { assertThat(config[newSpec.minSize], equalTo(2)) } } on("add repeated item") { it("should throw RepeatedItemException") { assertThat( { subject.addSpec(spec) }, throws( has( RepeatedItemException::name, equalTo(spec.qualify(size)) ) ) ) } } on("add repeated name") { val newSpec = ConfigSpec(prefix).apply { @Suppress("UNUSED_VARIABLE", "NAME_SHADOWING") val size by required() } it("should throw NameConflictException") { assertThat({ subject.addSpec(newSpec) }, throws()) } } on("add conflict name, which is prefix of existed name") { val newSpec = ConfigSpec().apply { @Suppress("UNUSED_VARIABLE") val buffer by required() } it("should throw NameConflictException") { assertThat( { subject.addSpec( newSpec.withPrefix(prefix.toPath().let { it.subList(0, it.size - 1) }.name) ) }, throws() ) } } on("add conflict name, and an existed name is prefix of it") { val newSpec = ConfigSpec(qualify(type.name)).apply { @Suppress("UNUSED_VARIABLE") val subType by required() } it("should throw NameConflictException") { assertThat({ subject.addSpec(newSpec) }, throws()) } } } group("addItem operation") { on("add orthogonal item") { val minSize by Spec.dummy.optional(1) val config = subject.withSource(mapOf(spec.qualify(minSize) to 2).asKVSource()) config.addItem(minSize, spec.prefix) it("should contain item") { assertTrue { minSize in config } assertTrue { spec.qualify(minSize) in config } assertThat(config.nameOf(minSize), equalTo(spec.qualify(minSize))) } it("should load values from the existed sources for item") { assertThat(config[minSize], equalTo(2)) } } on("add repeated item") { it("should throw RepeatedItemException") { assertThat( { subject.addItem(size, spec.prefix) }, throws( has( RepeatedItemException::name, equalTo(spec.qualify(size)) ) ) ) } } on("add repeated name") { @Suppress("NAME_SHADOWING") val size by Spec.dummy.required() it("should throw NameConflictException") { assertThat({ subject.addItem(size, prefix) }, throws()) } } on("add conflict name, which is prefix of existed name") { val buffer by Spec.dummy.required() it("should throw NameConflictException") { assertThat( { subject.addItem( buffer, prefix.toPath().let { it.subList(0, it.size - 1) }.name ) }, throws() ) } } on("add conflict name, and an existed name is prefix of it") { val subType by Spec.dummy.required() it("should throw NameConflictException") { assertThat({ subject.addItem(subType, qualify(type.name)) }, throws()) } } } on("iterate items in config") { it("should cover all items in config") { assertThat(subject.items.toSet(), equalTo(spec.items.toSet())) } } on("iterate name of items in config") { it("should cover all items in config") { assertThat(subject.nameOfItems.toSet(), equalTo(spec.items.map { qualify(it.name) }.toSet())) } } on("export values to map") { it("should not contain unset items in map") { assertThat( subject.toMap(), equalTo( mapOf( qualify(name.name) to "buffer", qualify(type.name) to NetworkBuffer.Type.OFF_HEAP.name, qualify(offset.name) to "null" ) ) ) } it("should contain corresponding items in map") { subject[size] = 4 subject[type] = NetworkBuffer.Type.ON_HEAP subject[offset] = 0 val map = subject.toMap() assertThat( map, equalTo( mapOf( qualify(size.name) to 4, qualify(maxSize.name) to 8, qualify(name.name) to "buffer", qualify(type.name) to NetworkBuffer.Type.ON_HEAP.name, qualify(offset.name) to 0 ) ) ) } it("should recover all items when reloaded from map") { subject[size] = 4 subject[type] = NetworkBuffer.Type.ON_HEAP subject[offset] = 0 val map = subject.toMap() val newConfig = Config { addSpec(spec[spec.prefix].withPrefix(prefix)) }.from.map.kv(map) assertThat(newConfig[size], equalTo(4)) assertThat(newConfig[maxSize], equalTo(8)) assertThat(newConfig[name], equalTo("buffer")) assertThat(newConfig[type], equalTo(NetworkBuffer.Type.ON_HEAP)) assertThat(newConfig[offset], equalTo(0)) assertThat(newConfig.toMap(), equalTo(subject.toMap())) } } on("export values to hierarchical map") { fun prefixToMap(prefix: String, value: Map): Map { return when { prefix.isEmpty() -> value prefix.contains('.') -> mapOf( prefix.substring(0, prefix.indexOf('.')) to prefixToMap(prefix.substring(prefix.indexOf('.') + 1), value) ) else -> mapOf(prefix to value) } } it("should not contain unset items in map") { assertThat( subject.toHierarchicalMap(), equalTo( prefixToMap( prefix, mapOf( "name" to "buffer", "type" to NetworkBuffer.Type.OFF_HEAP.name, "offset" to "null" ) ) ) ) } it("should contain corresponding items in map") { subject[size] = 4 subject[type] = NetworkBuffer.Type.ON_HEAP subject[offset] = 0 val map = subject.toHierarchicalMap() assertThat( map, equalTo( prefixToMap( prefix, mapOf( "size" to 4, "maxSize" to 8, "name" to "buffer", "type" to NetworkBuffer.Type.ON_HEAP.name, "offset" to 0 ) ) ) ) } it("should recover all items when reloaded from map") { subject[size] = 4 subject[type] = NetworkBuffer.Type.ON_HEAP subject[offset] = 0 val map = subject.toHierarchicalMap() val newConfig = Config { addSpec(spec[spec.prefix].withPrefix(prefix)) }.from.map.hierarchical(map) assertThat(newConfig[size], equalTo(4)) assertThat(newConfig[maxSize], equalTo(8)) assertThat(newConfig[name], equalTo("buffer")) assertThat(newConfig[type], equalTo(NetworkBuffer.Type.ON_HEAP)) assertThat(newConfig[offset], equalTo(0)) assertThat(newConfig.toMap(), equalTo(subject.toMap())) } } on("object methods") { val map = mapOf( qualify(name.name) to "buffer", qualify(type.name) to NetworkBuffer.Type.OFF_HEAP.name, qualify(offset.name) to "null" ) it("should not equal to object of other class") { assertFalse(subject.equals(1)) } it("should equal to itself") { assertThat(subject, equalTo(subject)) } it("should convert to string in map-like format") { assertThat(subject.toString(), equalTo("Config(items=$map)")) } } on("lock config") { it("should be locked") { subject.lock { } } } group("get operation") { on("get with valid item") { it("should return corresponding value") { assertThat(subject[name], equalTo("buffer")) assertTrue { name in subject } assertNull(subject[offset]) assertTrue { offset in subject } assertNull(subject.getOrNull(maxSize)) assertTrue { maxSize in subject } } } on("get with invalid item") { it("should throw NoSuchItemException when using `get`") { assertThat( { subject[invalidItem] }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } it("should return null when using `getOrNull`") { assertThat(subject.getOrNull(invalidItem), absent()) assertTrue { invalidItem !in subject } } } on("get with valid name") { it("should return corresponding value") { assertThat(subject(qualify("name")), equalTo("buffer")) assertThat(subject.getOrNull(qualify("name")), equalTo("buffer")) assertTrue { qualify("name") in subject } } } on("get with invalid name") { it("should throw NoSuchItemException when using `get`") { assertThat( { subject(spec.qualify(invalidItem)) }, throws( has( NoSuchItemException::name, equalTo(spec.qualify(invalidItem)) ) ) ) } it("should return null when using `getOrNull`") { assertThat(subject.getOrNull(spec.qualify(invalidItem)), absent()) assertTrue { spec.qualify(invalidItem) !in subject } } } on("get unset item") { it("should throw UnsetValueException") { assertThat( { subject[size] }, throws( has( UnsetValueException::name, equalTo(size.asName) ) ) ) assertThat( { subject[maxSize] }, throws( has( UnsetValueException::name, equalTo(size.asName) ) ) ) assertTrue { size in subject } assertTrue { maxSize in subject } } } on("get with lazy item that returns null when the type is nullable") { it("should return null") { val lazyItem by Spec.dummy.lazy { null } subject.addItem(lazyItem, prefix) assertNull(subject[lazyItem]) } } on("get with lazy item that returns null when the type is not nullable") { it("should throw InvalidLazySetException") { @Suppress("UNCHECKED_CAST") val thunk = { _: ItemContainer -> null } as (ItemContainer) -> Int val lazyItem by Spec.dummy.lazy(thunk = thunk) subject.addItem(lazyItem, prefix) assertThat({ subject[lazyItem] }, throws()) } } } group("set operation") { on("set with valid item when corresponding value is unset") { subject[size] = 1024 it("should contain the specified value") { assertThat(subject[size], equalTo(1024)) } } on("set with valid item when corresponding value exists") { it("should contain the specified value") { subject[name] = "newName" assertThat(subject[name], equalTo("newName")) subject[offset] = 0 assertThat(subject[offset], equalTo(0)) subject[offset] = null assertNull(subject[offset]) } } on("raw set with valid item") { it("should contain the specified value") { subject.rawSet(size, 2048) assertThat(subject[size], equalTo(2048)) } } on("set with valid item when corresponding value is lazy") { test( "before set, the item should be lazy; after set," + " the item should be no longer lazy, and it contains the specified value" ) { subject[size] = 1024 assertThat(subject[maxSize], equalTo(subject[size] * 2)) subject[maxSize] = 0 assertThat(subject[maxSize], equalTo(0)) subject[size] = 2048 assertThat(subject[maxSize], !equalTo(subject[size] * 2)) assertThat(subject[maxSize], equalTo(0)) } } on("set with invalid item") { it("should throw NoSuchItemException") { assertThat( { subject[invalidItem] = 1024 }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } } on("set with valid name") { subject[qualify("size")] = 1024 it("should contain the specified value") { assertThat(subject[size], equalTo(1024)) } } on("set with invalid name") { it("should throw NoSuchItemException") { assertThat( { subject[invalidItemName] = 1024 }, throws(has(NoSuchItemException::name, equalTo(invalidItemName))) ) } } on("set with incorrect type of value") { it("should throw ClassCastException") { assertThat({ subject[qualify(size.name)] = "1024" }, throws()) assertThat({ subject[qualify(size.name)] = null }, throws()) } } on("lazy set with valid item") { subject.lazySet(maxSize) { it[size] * 4 } subject[size] = 1024 it("should contain the specified value") { assertThat(subject[maxSize], equalTo(subject[size] * 4)) } } on("lazy set with invalid item") { it("should throw NoSuchItemException") { assertThat( { subject.lazySet(invalidItem) { 1024 } }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } } on("lazy set with valid name") { subject.lazySet(qualify(maxSize.name)) { it[size] * 4 } subject[size] = 1024 it("should contain the specified value") { assertThat(subject[maxSize], equalTo(subject[size] * 4)) } } on("lazy set with valid name and invalid value with incompatible type") { subject.lazySet(qualify(maxSize.name)) { "string" } it("should throw InvalidLazySetException when getting") { assertThat({ subject[qualify(maxSize.name)] }, throws()) } } on("lazy set with invalid name") { it("should throw NoSuchItemException") { assertThat( { subject.lazySet(invalidItemName) { 1024 } }, throws(has(NoSuchItemException::name, equalTo(invalidItemName))) ) } } on("unset with valid item") { subject.unset(type) it("should contain `null` when using `getOrNull`") { assertThat(subject.getOrNull(type), absent()) } } on("unset with invalid item") { it("should throw NoSuchItemException") { assertThat( { subject.unset(invalidItem) }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } } on("unset with valid name") { subject.unset(qualify(type.name)) it("should contain `null` when using `getOrNull`") { assertThat(subject.getOrNull(type), absent()) } } on("unset with invalid name") { it("should throw NoSuchItemException") { assertThat( { subject.unset(invalidItemName) }, throws(has(NoSuchItemException::name, equalTo(invalidItemName))) ) } } } on("clear operation") { it("should contain no value") { val config = if (subject.name == "multi-layer") { subject.parent!! } else { subject } assertTrue { name in config && type in config } config.clear() assertTrue { name !in config && type !in config } } } group("item property") { on("declare a property by item") { var nameProperty by subject.property(name) it("should behave same as `get`") { assertThat(nameProperty, equalTo(subject[name])) } it("should support set operation as `set`") { nameProperty = "newName" assertThat(nameProperty, equalTo("newName")) } } on("declare a property by invalid item") { it("should throw NoSuchItemException") { assertThat( { @Suppress("UNUSED_VARIABLE") var nameProperty by subject.property(invalidItem) }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } } on("declare a property by name") { var nameProperty by subject.property(qualify(name.name)) it("should behave same as `get`") { assertThat(nameProperty, equalTo(subject[name])) } it("should support set operation as `set`") { nameProperty = "newName" assertThat(nameProperty, equalTo("newName")) } } on("declare a property by invalid name") { it("should throw NoSuchItemException") { assertThat( { @Suppress("UNUSED_VARIABLE") var nameProperty by subject.property(invalidItemName) }, throws(has(NoSuchItemException::name, equalTo(invalidItemName))) ) } } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/ConfigSpecTestSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.fasterxml.jackson.databind.type.TypeFactory import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.has import com.natpryce.hamkrest.isIn import com.natpryce.hamkrest.sameInstance import com.natpryce.hamkrest.throws import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import kotlin.test.assertFalse import kotlin.test.assertTrue object ConfigSpecTestSpec : Spek({ given("a configSpec") { fun testItem(spec: Spec, item: Item<*>, description: String) { group("for $description, as an item") { on("add to a configSpec") { it("should be in the spec") { assertThat(item, isIn(spec.items)) } it("should have the specified description") { assertThat(item.description, equalTo("description")) } it("should name without prefix") { assertThat(item.name, equalTo("c.int")) } it("should have a valid path") { assertThat(item.path, equalTo(listOf("c", "int"))) } it("should point to the spec") { assertThat(item.spec, equalTo(spec)) } it("should have specified type") { assertThat( item.type, equalTo( TypeFactory.defaultInstance() .constructType(Int::class.javaObjectType) ) ) } } } } val specForRequired = object : ConfigSpec("a.b") { val item by required("c.int", "description") } testItem(specForRequired, specForRequired.item, "a required item") group("for a required item") { val spec = specForRequired on("add to a configSpec") { it("should still be a required item") { assertFalse(spec.item.nullable) assertTrue(spec.item.isRequired) assertFalse(spec.item.isOptional) assertFalse(spec.item.isLazy) assertThat(spec.item.asRequiredItem, sameInstance(spec.item)) assertThat({ spec.item.asOptionalItem }, throws()) assertThat({ spec.item.asLazyItem }, throws()) } } } val specForOptional = object : ConfigSpec("a.b") { val item by optional(1, "c.int", "description") } testItem(specForOptional, specForOptional.item, "an optional item") group("for an optional item") { val spec = specForOptional on("add to a configSpec") { it("should still be an optional item") { assertFalse(spec.item.nullable) assertFalse(spec.item.isRequired) assertTrue(spec.item.isOptional) assertFalse(spec.item.isLazy) assertThat({ spec.item.asRequiredItem }, throws()) assertThat(spec.item.asOptionalItem, sameInstance(spec.item)) assertThat({ spec.item.asLazyItem }, throws()) } it("should contain the specified default value") { assertThat(spec.item.default, equalTo(1)) } } } val specForLazy = object : ConfigSpec("a.b") { val item by lazy("c.int", "description") { 2 } } val config = Config { addSpec(specForLazy) } testItem(specForLazy, specForLazy.item, "a lazy item") group("for a lazy item") { val spec = specForLazy on("add to a configSpec") { it("should still be a lazy item") { assertTrue(spec.item.nullable) assertFalse(spec.item.isRequired) assertFalse(spec.item.isOptional) assertTrue(spec.item.isLazy) assertThat({ spec.item.asRequiredItem }, throws()) assertThat({ spec.item.asOptionalItem }, throws()) assertThat(spec.item.asLazyItem, sameInstance(spec.item)) } it("should contain the specified thunk") { assertThat(specForLazy.item.thunk(config), equalTo(2)) } } } on("add repeated item") { val spec = ConfigSpec() val item by Spec.dummy.required() spec.addItem(item) it("should throw RepeatedItemException") { assertThat( { spec.addItem(item) }, throws(has(RepeatedItemException::name, equalTo("item"))) ) } } on("add inner spec") { val spec = ConfigSpec() val innerSpec: Spec = ConfigSpec() spec.addInnerSpec(innerSpec) it("should contain the added spec") { assertThat(spec.innerSpecs, equalTo(setOf(innerSpec))) } it("should throw RepeatedInnerSpecException when adding repeated spec") { assertThat( { spec.addInnerSpec(innerSpec) }, throws(has(RepeatedInnerSpecException::spec, equalTo(innerSpec))) ) } } val spec = Nested group("get operation") { on("get an empty path") { it("should return itself") { assertThat(spec[""], equalTo(spec)) } } on("get a valid path") { it("should return a config spec with proper prefix") { assertThat(spec["a"].prefix, equalTo("bb")) assertThat(spec["a.bb"].prefix, equalTo("")) } it("should return a config spec with the proper items and inner specs") { assertThat(spec["a"].items, equalTo(spec.items)) assertThat(spec["a"].innerSpecs, equalTo(spec.innerSpecs)) assertThat(spec["a.bb.inner"].items, equalTo(Nested.Inner.items)) assertThat(spec["a.bb.inner"].innerSpecs.size, equalTo(0)) assertThat(spec["a.bb.inner"].prefix, equalTo("")) assertThat(spec["a.bb.inner2"].items, equalTo(Nested.Inner2.items)) assertThat(spec["a.bb.inner2"].innerSpecs.size, equalTo(0)) assertThat(spec["a.bb.inner2"].prefix, equalTo("level2")) assertThat(spec["a.bb.inner3"].items.size, equalTo(0)) assertThat(spec["a.bb.inner3"].innerSpecs.size, equalTo(2)) assertThat(spec["a.bb.inner3"].innerSpecs.toList()[0].prefix, equalTo("a")) assertThat(spec["a.bb.inner3"].innerSpecs.toList()[1].prefix, equalTo("b")) } } on("get an invalid path") { it("should throw NoSuchPathException") { assertThat({ spec["b"] }, throws(has(NoSuchPathException::path, equalTo("b")))) assertThat({ spec["a."] }, throws()) assertThat({ spec["a.b"] }, throws(has(NoSuchPathException::path, equalTo("a.b")))) assertThat( { spec["a.bb.inner4"] }, throws(has(NoSuchPathException::path, equalTo("a.bb.inner4"))) ) } } } group("prefix operation") { on("prefix with an empty path") { it("should return itself") { assertThat(Prefix("") + spec, equalTo(spec)) } } on("prefix with a non-empty path") { it("should return a config spec with proper prefix") { assertThat((Prefix("c") + spec).prefix, equalTo("c.a.bb")) assertThat((Prefix("c") + spec["a.bb"]).prefix, equalTo("c")) } it("should return a config spec with the same items and inner specs") { assertThat((Prefix("c") + spec).items, equalTo(spec.items)) assertThat((Prefix("c") + spec).innerSpecs, equalTo(spec.innerSpecs)) } } } group("plus operation") { val spec1 = object : ConfigSpec("a") { val item1 by required() } val spec2 = object : ConfigSpec("b") { val item2 by required() } @Suppress("NAME_SHADOWING") val spec by memoized { spec1 + spec2 } on("add a valid item") { it("should contains the item in the facade spec") { val item by Spec.dummy.required() spec.addItem(item) assertThat(item, isIn(spec.items)) assertThat(item, isIn(spec2.items)) } } on("add a repeated item") { it("should throw RepeatedItemException") { assertThat( { spec.addItem(spec1.item1) }, throws(has(RepeatedItemException::name, equalTo("item1"))) ) } } on("get the list of items") { it("should contains all items in both the facade spec and the fallback spec") { assertThat(spec.items, equalTo(spec1.items + spec2.items)) } } on("qualify item name") { it("should add proper prefix") { assertThat(spec.qualify(spec1.item1), equalTo("a.item1")) assertThat(spec.qualify(spec2.item2), equalTo("b.item2")) } } } group("withFallback operation") { val spec1 = object : ConfigSpec("a") { val item1 by required() } val spec2 = object : ConfigSpec("b") { val item2 by required() } @Suppress("NAME_SHADOWING") val spec by memoized { spec2.withFallback(spec1) } on("add a valid item") { it("should contains the item in the facade spec") { val item by Spec.dummy.required() spec.addItem(item) assertThat(item, isIn(spec.items)) assertThat(item, isIn(spec2.items)) } } on("add a repeated item") { it("should throw RepeatedItemException") { assertThat( { spec.addItem(spec1.item1) }, throws(has(RepeatedItemException::name, equalTo("item1"))) ) } } on("get the list of items") { it("should contains all items in both the facade spec and the fallback spec") { assertThat(spec.items, equalTo(spec1.items + spec2.items)) } } on("qualify item name") { it("should add proper prefix") { assertThat(spec.qualify(spec1.item1), equalTo("a.item1")) assertThat(spec.qualify(spec2.item2), equalTo("b.item2")) } } } group("prefix inference") { val configSpecInstance = ConfigSpec() on("instance of `ConfigSpec` class") { it("should inference prefix as \"\"") { assertThat(configSpecInstance.prefix, equalTo("")) } } on("anonymous class") { it("should inference prefix as \"\"") { assertThat(AnonymousConfigSpec.spec.prefix, equalTo("")) } } val objectExpression = object : ConfigSpec() {} on("object expression") { it("should inference prefix as \"\"") { assertThat(objectExpression.prefix, equalTo("")) } } on("class with uppercase capital") { it("should inference prefix as the class name with lowercase capital") { assertThat(Uppercase.prefix, equalTo("uppercase")) } } on("class with uppercase name") { it("should inference prefix as the lowercase class name") { assertThat(OK.prefix, equalTo("ok")) } } on("class with uppercase first word") { it("should inference prefix as the class name with lowercase first word") { assertThat(TCPService.prefix, equalTo("tcpService")) } } on("class with lowercase capital") { it("should inference prefix as the class name") { assertThat(lowercase.prefix, equalTo("lowercase")) } } on("class with \"Spec\" suffix") { it("should inference prefix as the class name without the suffix") { assertThat(SuffixSpec.prefix, equalTo("suffix")) } } on("companion object of a class") { it("should inference prefix as the class name") { assertThat(OriginalSpec.prefix, equalTo("original")) } } } } }) object Nested : ConfigSpec("a.bb") { val item by required("int", "description") object Inner : ConfigSpec() { val item by required() } object Inner2 : ConfigSpec("inner2.level2") { val item by required() } object Inner3a : ConfigSpec("inner3.a") { val item by required() } object Inner3b : ConfigSpec("inner3.b") { val item by required() } } object Uppercase : ConfigSpec() object OK : ConfigSpec() object TCPService : ConfigSpec() @Suppress("ClassName") object lowercase : ConfigSpec() object SuffixSpec : ConfigSpec() class OriginalSpec { companion object : ConfigSpec() } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/ConfigTestSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.absent import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.has import com.natpryce.hamkrest.sameInstance import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.base.asKVSource import com.uchuhimo.konf.source.base.toHierarchicalMap import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.dsl.SubjectProviderDsl import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue object ConfigTestSpec : SubjectSpek({ subject { Config { addSpec(NetworkBuffer) } } configTestSpec() }) fun SubjectProviderDsl.configTestSpec(prefix: String = "network.buffer") { val spec = NetworkBuffer val size = NetworkBuffer.size val maxSize = NetworkBuffer.maxSize val name = NetworkBuffer.name val type = NetworkBuffer.type val offset = NetworkBuffer.offset fun qualify(name: String): String = if (prefix.isEmpty()) name else "$prefix.$name" given("a config") { val invalidItem by ConfigSpec("invalid").required() val invalidItemName = "invalid.invalidItem" group("feature operation") { on("enable feature") { subject.enable(Feature.FAIL_ON_UNKNOWN_PATH) it("should let the feature be enabled") { assertTrue { subject.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH) } } } on("disable feature") { subject.disable(Feature.FAIL_ON_UNKNOWN_PATH) it("should let the feature be disabled") { assertFalse { subject.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH) } } } on("by default") { it("should use the feature's default setting") { assertThat( subject.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH), equalTo(Feature.FAIL_ON_UNKNOWN_PATH.enabledByDefault) ) } } } group("subscribe operation") { on("load source when subscriber is defined") { var loadFunction: (source: Source) -> Unit = {} var counter = 0 val config = subject.withLoadTrigger("") { _, load -> loadFunction = load }.withLayer() val source = mapOf(qualify(type.name) to NetworkBuffer.Type.ON_HEAP).asKVSource() val handler1 = config.beforeLoad { counter += 1 it("should contain the old value") { assertThat(it, equalTo(source)) assertThat(config[type], equalTo(NetworkBuffer.Type.OFF_HEAP)) } } val handler2 = config.beforeLoad { counter += 1 it("should contain the old value") { assertThat(it, equalTo(source)) assertThat(config[type], equalTo(NetworkBuffer.Type.OFF_HEAP)) } } val handler3 = config.afterLoad { counter += 1 it("should contain the new value") { assertThat(it, equalTo(source)) assertThat(config[type], equalTo(NetworkBuffer.Type.ON_HEAP)) } } val handler4 = config.afterLoad { counter += 1 it("should contain the new value") { assertThat(it, equalTo(source)) assertThat(config[type], equalTo(NetworkBuffer.Type.ON_HEAP)) } } loadFunction(source) handler1.close() handler2.close() handler3.close() handler4.close() it("should notify subscriber") { assertThat(counter, equalTo(4)) } } } group("addSpec operation") { on("add orthogonal spec") { val newSpec = object : ConfigSpec(spec.prefix) { val minSize by optional(1) } val config = subject.withSource(mapOf(newSpec.qualify(newSpec.minSize) to 2).asKVSource()) config.addSpec(newSpec) it("should contain items in new spec") { assertTrue { newSpec.minSize in config } assertTrue { newSpec.qualify(newSpec.minSize) in config } assertThat(config.nameOf(newSpec.minSize), equalTo(newSpec.qualify(newSpec.minSize))) } it("should contain new spec") { assertThat(newSpec in config.specs, equalTo(true)) assertThat(spec in config.specs, equalTo(true)) } it("should load values from the existed sources for items in new spec") { assertThat(config[newSpec.minSize], equalTo(2)) } } on("add spec with inner specs") { subject.addSpec(Service) it("should contain items in new spec") { assertTrue { Service.name in subject } assertTrue { Service.UI.host in subject } assertTrue { Service.UI.port in subject } assertTrue { Service.Backend.host in subject } assertTrue { Service.Backend.port in subject } assertTrue { Service.Backend.Login.user in subject } assertTrue { Service.Backend.Login.password in subject } assertTrue { "service.name" in subject } assertTrue { "service.ui.host" in subject } assertTrue { "service.ui.port" in subject } assertTrue { "service.backend.host" in subject } assertTrue { "service.backend.port" in subject } assertTrue { "service.backend.login.user" in subject } assertTrue { "service.backend.login.password" in subject } } it("should contain new spec") { assertTrue { Service in subject.specs } } it("should not contain inner specs in new spec") { assertFalse { Service.UI in subject.specs } assertFalse { Service.Backend in subject.specs } assertFalse { Service.Backend.Login in subject.specs } } } on("add nested spec") { subject.addSpec(Service.Backend) it("should contain items in the nested spec") { assertTrue { Service.Backend.host in subject } assertTrue { Service.Backend.port in subject } assertTrue { Service.Backend.Login.user in subject } assertTrue { Service.Backend.Login.password in subject } } it("should not contain items in the outer spec") { assertFalse { Service.name in subject } assertFalse { Service.UI.host in subject } assertFalse { Service.UI.port in subject } } it("should contain the nested spec") { assertTrue { Service.Backend in subject.specs } } it("should not contain the outer spec or inner specs in the nested spec") { assertFalse { Service in subject.specs } assertFalse { Service.UI in subject.specs } assertFalse { Service.Backend.Login in subject.specs } } } on("add repeated item") { it("should throw RepeatedItemException") { assertThat( { subject.addSpec(spec) }, throws( has( RepeatedItemException::name, equalTo(spec.qualify(size)) ) ) ) } } on("add repeated name") { val newSpec = ConfigSpec(prefix).apply { @Suppress("UNUSED_VARIABLE", "NAME_SHADOWING") val size by required() } it("should throw NameConflictException") { assertThat({ subject.addSpec(newSpec) }, throws()) } } on("add conflict name, which is prefix of existed name") { val newSpec = ConfigSpec().apply { @Suppress("UNUSED_VARIABLE") val buffer by required() } it("should throw NameConflictException") { assertThat( { subject.addSpec( newSpec.withPrefix(prefix.toPath().let { it.subList(0, it.size - 1) }.name) ) }, throws() ) } } on("add conflict name, and an existed name is prefix of it") { val newSpec = ConfigSpec(qualify(type.name)).apply { @Suppress("UNUSED_VARIABLE") val subType by required() } it("should throw NameConflictException") { assertThat({ subject.addSpec(newSpec) }, throws()) } } } group("addItem operation") { on("add orthogonal item") { val minSize by Spec.dummy.optional(1) val config = subject.withSource(mapOf(qualify(minSize.name) to 2).asKVSource()) config.addItem(minSize, prefix) it("should contain item") { assertTrue { minSize in config } assertTrue { qualify(minSize.name) in config } assertThat(config.nameOf(minSize), equalTo(qualify(minSize.name))) } it("should load values from the existed sources for item") { assertThat(config[minSize], equalTo(2)) } } on("add repeated item") { it("should throw RepeatedItemException") { assertThat( { subject.addItem(size, prefix) }, throws( has( RepeatedItemException::name, equalTo(qualify(size.name)) ) ) ) } } on("add repeated name") { @Suppress("NAME_SHADOWING") val size by Spec.dummy.required() it("should throw NameConflictException") { assertThat({ subject.addItem(size, prefix) }, throws()) } } on("add conflict name, which is prefix of existed name") { val buffer by Spec.dummy.required() it("should throw NameConflictException") { assertThat( { subject.addItem( buffer, prefix.toPath().let { it.subList(0, it.size - 1) }.name ) }, throws() ) } } on("add conflict name, and an existed name is prefix of it") { val subType by Spec.dummy.required() it("should throw NameConflictException") { assertThat({ subject.addItem(subType, qualify(type.name)) }, throws()) } } } on("iterate items in config") { it("should cover all items in config") { assertThat(subject.items.toSet(), equalTo(spec.items.toSet())) } } on("iterate name of items in config") { it("should cover all items in config") { assertThat(subject.nameOfItems.toSet(), equalTo(spec.items.map { qualify(it.name) }.toSet())) } } on("export values to map") { it("should not contain unset items in map") { assertThat( subject.toMap(), equalTo( mapOf( qualify(name.name) to "buffer", qualify(type.name) to NetworkBuffer.Type.OFF_HEAP.name, qualify(offset.name) to "null" ) ) ) } it("should contain corresponding items in map") { subject[size] = 4 subject[type] = NetworkBuffer.Type.ON_HEAP subject[offset] = 0 val map = subject.toMap() assertThat( map, equalTo( mapOf( qualify(size.name) to 4, qualify(maxSize.name) to 8, qualify(name.name) to "buffer", qualify(type.name) to NetworkBuffer.Type.ON_HEAP.name, qualify(offset.name) to 0 ) ) ) } it("should recover all items when reloaded from map") { subject[size] = 4 subject[type] = NetworkBuffer.Type.ON_HEAP subject[offset] = 0 val map = subject.toMap() val newConfig = Config { addSpec(spec[spec.prefix].withPrefix(prefix)) }.from.map.kv(map) assertThat(newConfig[size], equalTo(4)) assertThat(newConfig[maxSize], equalTo(8)) assertThat(newConfig[name], equalTo("buffer")) assertThat(newConfig[type], equalTo(NetworkBuffer.Type.ON_HEAP)) assertThat(newConfig[offset], equalTo(0)) assertThat(newConfig.toMap(), equalTo(subject.toMap())) } } on("export values to hierarchical map") { fun prefixToMap(prefix: String, value: Map): Map { return when { prefix.isEmpty() -> value prefix.contains('.') -> mapOf( prefix.substring(0, prefix.indexOf('.')) to prefixToMap(prefix.substring(prefix.indexOf('.') + 1), value) ) else -> mapOf(prefix to value) } } it("should not contain unset items in map") { assertThat( subject.toHierarchicalMap(), equalTo( prefixToMap( prefix, mapOf( "name" to "buffer", "type" to NetworkBuffer.Type.OFF_HEAP.name, "offset" to "null" ) ) ) ) } it("should contain corresponding items in map") { subject[size] = 4 subject[type] = NetworkBuffer.Type.ON_HEAP subject[offset] = 0 val map = subject.toHierarchicalMap() assertThat( map, equalTo( prefixToMap( prefix, mapOf( "size" to 4, "maxSize" to 8, "name" to "buffer", "type" to NetworkBuffer.Type.ON_HEAP.name, "offset" to 0 ) ) ) ) } it("should recover all items when reloaded from map") { subject[size] = 4 subject[type] = NetworkBuffer.Type.ON_HEAP subject[offset] = 0 val map = subject.toHierarchicalMap() val newConfig = Config { addSpec(spec[spec.prefix].withPrefix(prefix)) }.from.map.hierarchical(map) assertThat(newConfig[size], equalTo(4)) assertThat(newConfig[maxSize], equalTo(8)) assertThat(newConfig[name], equalTo("buffer")) assertThat(newConfig[type], equalTo(NetworkBuffer.Type.ON_HEAP)) assertThat(newConfig[offset], equalTo(0)) assertThat(newConfig.toMap(), equalTo(subject.toMap())) } } on("object methods") { val map = mapOf( qualify(name.name) to "buffer", qualify(type.name) to NetworkBuffer.Type.OFF_HEAP.name, qualify(offset.name) to "null" ) it("should not equal to object of other class") { assertFalse(subject.equals(1)) } it("should equal to itself") { assertThat(subject, equalTo(subject)) } it("should convert to string in map-like format") { assertThat(subject.toString(), equalTo("Config(items=$map)")) } } on("lock config") { it("should be locked") { subject.lock { } } } group("get operation") { on("get with valid item") { it("should return corresponding value") { assertThat(subject[name], equalTo("buffer")) assertTrue { name in subject } assertNull(subject[offset]) assertTrue { offset in subject } assertNull(subject.getOrNull(maxSize)) assertTrue { maxSize in subject } } } on("get with invalid item") { it("should throw NoSuchItemException when using `get`") { assertThat( { subject[invalidItem] }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } it("should return null when using `getOrNull`") { assertThat(subject.getOrNull(invalidItem), absent()) assertTrue { invalidItem !in subject } } } on("get with valid name") { it("should return corresponding value") { assertThat(subject(qualify("name")), equalTo("buffer")) assertThat(subject.getOrNull(qualify("name")), equalTo("buffer")) assertTrue { qualify("name") in subject } } } on("get with valid name which contains trailing whitespaces") { it("should return corresponding value") { assertThat(subject(qualify("name ")), equalTo("buffer")) assertThat(subject.getOrNull(qualify("name ")), equalTo("buffer")) assertTrue { qualify("name ") in subject } } } on("get with invalid name") { it("should throw NoSuchItemException when using `get`") { assertThat( { subject(qualify(invalidItem.name)) }, throws( has( NoSuchItemException::name, equalTo(qualify(invalidItem.name)) ) ) ) } it("should return null when using `getOrNull`") { assertThat(subject.getOrNull(qualify(invalidItem.name)), absent()) assertTrue { qualify(invalidItem.name) !in subject } } } on("get unset item") { it("should throw UnsetValueException") { assertThat( { subject[size] }, throws( has( UnsetValueException::name, equalTo(size.asName) ) ) ) assertThat( { subject[maxSize] }, throws( has( UnsetValueException::name, equalTo(size.asName) ) ) ) assertTrue { size in subject } assertTrue { maxSize in subject } } } on("get with lazy item that returns null when the type is nullable") { it("should return null") { val lazyItem by Spec.dummy.lazy { null } subject.addItem(lazyItem, prefix) assertNull(subject[lazyItem]) } } on("get with lazy item that returns null when the type is not nullable") { it("should throw InvalidLazySetException") { @Suppress("UNCHECKED_CAST") val thunk = { _: ItemContainer -> null } as (ItemContainer) -> Int val lazyItem by Spec.dummy.lazy(thunk = thunk) subject.addItem(lazyItem, prefix) assertThat({ subject[lazyItem] }, throws()) } } } group("set operation") { on("set with valid item when corresponding value is unset") { subject[size] = 1024 it("should contain the specified value") { assertThat(subject[size], equalTo(1024)) } } on("set with valid item when corresponding value exists") { it("should contain the specified value") { subject[name] = "newName" assertThat(subject[name], equalTo("newName")) subject[offset] = 0 assertThat(subject[offset], equalTo(0)) subject[offset] = null assertNull(subject[offset]) } } on("raw set with valid item") { it("should contain the specified value") { subject.rawSet(size, 2048) assertThat(subject[size], equalTo(2048)) } } on("set with valid item when corresponding value is lazy") { test( "before set, the item should be lazy; after set," + " the item should be no longer lazy, and it contains the specified value" ) { subject[size] = 1024 assertThat(subject[maxSize], equalTo(subject[size] * 2)) subject[maxSize] = 0 assertThat(subject[maxSize], equalTo(0)) subject[size] = 2048 assertThat(subject[maxSize], !equalTo(subject[size] * 2)) assertThat(subject[maxSize], equalTo(0)) } } on("set with invalid item") { it("should throw NoSuchItemException") { assertThat( { subject[invalidItem] = 1024 }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } } on("set with valid name") { subject[qualify("size")] = 1024 it("should contain the specified value") { assertThat(subject[size], equalTo(1024)) } } on("set with valid name which contains trailing whitespaces") { subject[qualify("size ")] = 1024 it("should contain the specified value") { assertThat(subject[size], equalTo(1024)) } } on("set with invalid name") { it("should throw NoSuchItemException") { assertThat( { subject[invalidItemName] = 1024 }, throws(has(NoSuchItemException::name, equalTo(invalidItemName))) ) } } on("set with incorrect type of value") { it("should throw ClassCastException") { assertThat({ subject[qualify(size.name)] = "1024" }, throws()) assertThat({ subject[qualify(size.name)] = null }, throws()) } } on("set when beforeSet subscriber is defined") { val childConfig = subject.withLayer() subject[size] = 1 var counter = 0 val handler1 = childConfig.beforeSet { item, value -> counter += 1 it("should contain the old value") { assertThat(item, equalTo(size)) assertThat(value, equalTo(2)) assertThat(childConfig[size], equalTo(1)) } } val handler2 = childConfig.beforeSet { item, value -> counter += 1 it("should contain the old value") { assertThat(item, equalTo(size)) assertThat(value, equalTo(2)) assertThat(childConfig[size], equalTo(1)) } } val handler3 = size.beforeSet { _, value -> counter += 1 it("should contain the old value") { assertThat(value, equalTo(2)) assertThat(childConfig[size], equalTo(1)) } } val handler4 = size.beforeSet { _, value -> counter += 1 it("should contain the old value") { assertThat(value, equalTo(2)) assertThat(childConfig[size], equalTo(1)) } } subject[size] = 2 handler1.close() handler2.close() handler3.close() handler4.close() it("should notify subscriber") { assertThat(counter, equalTo(4)) } } on("set when afterSet subscriber is defined") { val childConfig = subject.withLayer() subject[size] = 1 var counter = 0 val handler1 = childConfig.afterSet { item, value -> counter += 1 it("should contain the new value") { assertThat(item, equalTo(size)) assertThat(value, equalTo(2)) assertThat(childConfig[size], equalTo(2)) } } val handler2 = childConfig.afterSet { item, value -> counter += 1 it("should contain the new value") { assertThat(item, equalTo(size)) assertThat(value, equalTo(2)) assertThat(childConfig[size], equalTo(2)) } } val handler3 = size.afterSet { _, value -> counter += 1 it("should contain the new value") { assertThat(value, equalTo(2)) assertThat(childConfig[size], equalTo(2)) } } val handler4 = size.afterSet { _, value -> counter += 1 it("should contain the new value") { assertThat(value, equalTo(2)) assertThat(childConfig[size], equalTo(2)) } } subject[size] = 2 handler1.close() handler2.close() handler3.close() handler4.close() it("should notify subscriber") { assertThat(counter, equalTo(4)) } } on("set when onSet subscriber is defined") { var counter = 0 size.onSet { counter += 1 }.use { subject[size] = 1 subject[size] = 16 subject[size] = 256 subject[size] = 1024 it("should notify subscriber") { assertThat(counter, equalTo(4)) } } } on("set when multiple onSet subscribers are defined") { var counter = 0 size.onSet { counter += 1 }.use { size.onSet { counter += 2 }.use { subject[size] = 1 subject[size] = 16 subject[size] = 256 subject[size] = 1024 it("should notify all subscribers") { assertThat(counter, equalTo(12)) } } } } on("lazy set with valid item") { subject.lazySet(maxSize) { it[size] * 4 } subject[size] = 1024 it("should contain the specified value") { assertThat(subject[maxSize], equalTo(subject[size] * 4)) } } on("lazy set with invalid item") { it("should throw NoSuchItemException") { assertThat( { subject.lazySet(invalidItem) { 1024 } }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } } on("lazy set with valid name") { subject.lazySet(qualify(maxSize.name)) { it[size] * 4 } subject[size] = 1024 it("should contain the specified value") { assertThat(subject[maxSize], equalTo(subject[size] * 4)) } } on("lazy set with valid name which contains trailing whitespaces") { subject.lazySet(qualify(maxSize.name + " ")) { it[size] * 4 } subject[size] = 1024 it("should contain the specified value") { assertThat(subject[maxSize], equalTo(subject[size] * 4)) } } on("lazy set with valid name and invalid value with incompatible type") { subject.lazySet(qualify(maxSize.name)) { "string" } it("should throw InvalidLazySetException when getting") { assertThat({ subject[qualify(maxSize.name)] }, throws()) } } on("lazy set with invalid name") { it("should throw NoSuchItemException") { assertThat( { subject.lazySet(invalidItemName) { 1024 } }, throws(has(NoSuchItemException::name, equalTo(invalidItemName))) ) } } on("unset with valid item") { subject.unset(type) it("should contain `null` when using `getOrNull`") { assertThat(subject.getOrNull(type), absent()) } } on("unset with invalid item") { it("should throw NoSuchItemException") { assertThat( { subject.unset(invalidItem) }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } } on("unset with valid name") { subject.unset(qualify(type.name)) it("should contain `null` when using `getOrNull`") { assertThat(subject.getOrNull(type), absent()) } } on("unset with invalid name") { it("should throw NoSuchItemException") { assertThat( { subject.unset(invalidItemName) }, throws(has(NoSuchItemException::name, equalTo(invalidItemName))) ) } } } on("clear operation") { subject[size] = 1 subject[maxSize] = 4 it("should contain no value") { assertThat(subject[size], equalTo(1)) assertThat(subject[maxSize], equalTo(4)) subject.clear() assertNull(subject.getOrNull(size)) assertNull(subject.getOrNull(maxSize)) } } on("clear all operation") { it("should contain no value") { assertTrue { name in subject && type in subject } subject.clearAll() assertTrue { name !in subject && type !in subject } } } on("check whether all required items have values or not") { it("should return false when some required items don't have values") { assertFalse { subject.containsRequired() } } it("should return true when all required items have values") { subject[size] = 1 assertTrue { subject.containsRequired() } } } on("validate whether all required items have values or not") { it("should throw UnsetValueException when some required items don't have values") { assertThat( { subject.validateRequired() }, throws() ) } it("should return itself when all required items have values") { subject[size] = 1 assertThat(subject, sameInstance(subject.validateRequired())) } } group("item property") { on("declare a property by item") { var nameProperty by subject.property(name) it("should behave same as `get`") { assertThat(nameProperty, equalTo(subject[name])) } it("should support set operation as `set`") { nameProperty = "newName" assertThat(nameProperty, equalTo("newName")) } } on("declare a property by invalid item") { it("should throw NoSuchItemException") { assertThat( { @Suppress("UNUSED_VARIABLE") var nameProperty by subject.property(invalidItem) }, throws(has(NoSuchItemException::name, equalTo(invalidItem.asName))) ) } } on("declare a property by name") { var nameProperty by subject.property(qualify(name.name)) it("should behave same as `get`") { assertThat(nameProperty, equalTo(subject[name])) } it("should support set operation as `set`") { nameProperty = "newName" assertThat(nameProperty, equalTo("newName")) } } on("declare a property by invalid name") { it("should throw NoSuchItemException") { assertThat( { @Suppress("UNUSED_VARIABLE") var nameProperty by subject.property(invalidItemName) }, throws(has(NoSuchItemException::name, equalTo(invalidItemName))) ) } } } } } object Service : ConfigSpec() { val name by optional("test") object Backend : ConfigSpec() { val host by optional("127.0.0.1") val port by optional(7777) object Login : ConfigSpec() { val user by optional("admin") val password by optional("123456") } } object UI : ConfigSpec() { val host by optional("127.0.0.1") val port by optional(8888) } } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/FeatureSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.has import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.UnknownPathsException import com.uchuhimo.konf.source.asSource import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.junit.jupiter.api.assertThrows import java.io.FileNotFoundException object FailOnUnknownPathSpec : Spek({ //language=Json val source = """ { "level1": { "level2": { "valid": "value1", "invalid": "value2" } } } """.trimIndent() given("a config") { on("the feature is disabled") { val config = Config { addSpec(Valid) } it("should ignore unknown paths") { val conf = config.from.disabled(Feature.FAIL_ON_UNKNOWN_PATH).json.string(source) assertThat(conf[Valid.valid], equalTo("value1")) } } on("the feature is enabled on config") { val config = Config { addSpec(Valid) }.enable(Feature.FAIL_ON_UNKNOWN_PATH) it("should throws UnknownPathsException and reports the unknown paths") { assertThat( { config.from.json.string(source) }, throws( has( UnknownPathsException::paths, equalTo(listOf("level1.level2.invalid")) ) ) ) } } on("the feature is enabled on source") { val config = Config { addSpec(Valid) } it("should throws UnknownPathsException and reports the unknown paths") { assertThat( { config.from.enabled(Feature.FAIL_ON_UNKNOWN_PATH).json.string(source) }, throws( has( UnknownPathsException::paths, equalTo(listOf("level1.level2.invalid")) ) ) ) } } } }) object LoadKeysCaseInsensitivelySpec : Spek({ given("a config") { on("by default") { val source = mapOf("somekey" to "value").asSource() val config = Config().withSource(source) it("should load keys case-sensitively") { val someKey by config.required() assertThrows { someKey.isNotEmpty() } val somekey by config.required() assertThat(somekey, equalTo("value")) } } on("the feature is disabled") { val source = mapOf("somekey" to "value").asSource().disabled(Feature.LOAD_KEYS_CASE_INSENSITIVELY) val config = Config().withSource(source) it("should load keys case-sensitively") { val someKey by config.required() assertThrows { someKey.isNotEmpty() } val somekey by config.required() assertThat(somekey, equalTo("value")) } } on("the feature is enabled on config") { val source = mapOf("somekey" to "value").asSource() val config = Config().enable(Feature.LOAD_KEYS_CASE_INSENSITIVELY).withSource(source) it("should load keys case-insensitively") { val someKey by config.required() assertThat(someKey, equalTo("value")) } } on("the feature is enabled on source") { val source = mapOf("somekey" to "value").asSource().enabled(Feature.LOAD_KEYS_CASE_INSENSITIVELY) val config = Config().withSource(source) it("should load keys case-insensitively") { val someKey by config.required() assertThat(someKey, equalTo("value")) } } } }) object LoadKeysAsLittleCamelCaseSpec : Spek({ given("a config") { on("by default") { val source = mapOf( "some_key" to "value", "some_key2_" to "value", "_some_key3" to "value", "SomeKey4" to "value", "some_0key5" to "value", "some__key6" to "value", "some___key7" to "value", "some_some_key8" to "value", "some key9" to "value", "SOMEKey10" to "value" ).asSource() val config = Config().withSource(source) it("should load keys as little camel case") { val someKey by config.required() assertThat(someKey, equalTo("value")) val someKey2 by config.required() assertThat(someKey2, equalTo("value")) val someKey3 by config.required() assertThat(someKey3, equalTo("value")) val someKey4 by config.required() assertThat(someKey4, equalTo("value")) val some0key5 by config.required() assertThat(some0key5, equalTo("value")) val someKey6 by config.required() assertThat(someKey6, equalTo("value")) val someKey7 by config.required() assertThat(someKey7, equalTo("value")) val someSomeKey8 by config.required() assertThat(someSomeKey8, equalTo("value")) val someKey9 by config.required() assertThat(someKey9, equalTo("value")) val someKey10 by config.required() assertThat(someKey10, equalTo("value")) } } on("the feature is enabled") { val source = mapOf("some_key" to "value").asSource().enabled(Feature.LOAD_KEYS_AS_LITTLE_CAMEL_CASE) val config = Config().withSource(source) it("should load keys as little camel case") { val someKey by config.required() assertThat(someKey, equalTo("value")) } } on("the feature is disabled on config") { val source = mapOf("some_key" to "value").asSource() val config = Config().disable(Feature.LOAD_KEYS_AS_LITTLE_CAMEL_CASE).withSource(source) it("should load keys as usual") { val someKey by config.required() assertThrows { someKey.isNotEmpty() } val some_key by config.required() assertThat(some_key, equalTo("value")) } } on("the feature is disabled on source") { val source = mapOf("some_key" to "value").asSource().disabled(Feature.LOAD_KEYS_AS_LITTLE_CAMEL_CASE) val config = Config().withSource(source) it("should load keys as usual") { val someKey by config.required() assertThrows { someKey.isNotEmpty() } val some_key by config.required() assertThat(some_key, equalTo("value")) } } } }) object OptionalSourceByDefautSpec : Spek({ given("a config") { on("the feature is disabled") { val config = Config().disable(Feature.OPTIONAL_SOURCE_BY_DEFAULT) it("should throw exception when file is not existed") { assertThrows { config.from.file("not_existed.json") } } } on("the feature is enabled on config") { val config = Config().enable(Feature.OPTIONAL_SOURCE_BY_DEFAULT) it("should load empty source") { config.from.mapped { assertThat(it.tree.children, equalTo(mutableMapOf())) it }.file("not_existed.json") config.from.mapped { assertThat(it.tree.children, equalTo(mutableMapOf())) it }.json.file("not_existed.json") } } } }) private object Valid : ConfigSpec("level1.level2") { val valid by required() } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/MergedConfigSpek.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object FacadeConfigSpec : SubjectSpek({ subject { Config() + Config { addSpec(NetworkBuffer) } } configTestSpec() }) object MultiLayerFacadeConfigSpec : SubjectSpek({ subject { (Config() + Config { addSpec(NetworkBuffer) }).withLayer("multi-layer") } configTestSpec() }) object FacadeMultiLayerConfigSpec : SubjectSpek({ subject { Config() + Config { addSpec(NetworkBuffer) }.withLayer("multi-layer") } configTestSpec() }) object FacadeConfigUsingWithFallbackSpec : SubjectSpek({ subject { Config { addSpec(NetworkBuffer) }.withFallback(Config()) } configTestSpec() }) object FallbackConfigSpec : SubjectSpek({ subject { Config { addSpec(NetworkBuffer) } + Config() } configTestSpec() }) object MultiLayerFallbackConfigSpec : SubjectSpek({ subject { (Config { addSpec(NetworkBuffer) } + Config()).withLayer("multi-layer") } configTestSpec() }) object FallbackMultiLayerConfigSpec : SubjectSpek({ subject { Config { addSpec(NetworkBuffer) }.withLayer("multi-layer") + Config() } configTestSpec() }) object FallbackConfigUsingWithFallbackSpec : SubjectSpek({ subject { Config().withFallback(Config { addSpec(NetworkBuffer) }) } configTestSpec() }) object BothConfigSpec : SubjectSpek({ subject { Config { addSpec(NetworkBuffer) } + Config { addSpec(NetworkBuffer) } } configTestSpec() given("a merged config") { on("set item in the fallback config") { (subject as MergedConfig).fallback[NetworkBuffer.type] = NetworkBuffer.Type.ON_HEAP it("should have higher priority than the default value") { assertThat((subject as MergedConfig)[NetworkBuffer.type], equalTo(NetworkBuffer.Type.ON_HEAP)) } } } }) class UpdateFallbackConfig(val config: MergedConfig) : MergedConfig(config.facade, config.fallback) { override fun rawSet(item: Item<*>, value: Any?) { if (item is LazyItem) { facade.rawSet(item, value) } else { fallback.rawSet(item, value) } } override fun unset(item: Item<*>) { fallback.unset(item) } override fun addItem(item: Item<*>, prefix: String) { fallback.addItem(item, prefix) } override fun addSpec(spec: Spec) { fallback.addSpec(spec) } } object UpdateFallbackConfigSpec : SubjectSpek({ subject { UpdateFallbackConfig((Config { addSpec(NetworkBuffer) } + Config { addSpec(NetworkBuffer) }) as MergedConfig) } configTestSpec() }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/MergedMapSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue object MergedMapSpec : SubjectSpek>({ subject { val facadeMap = mutableMapOf("a" to 1, "b" to 2) val fallbackMap = mutableMapOf("b" to 3, "c" to 4) MergedMap(fallback = fallbackMap, facade = facadeMap) } given("a merged map") { val mergedMap = mapOf("a" to 1, "b" to 2, "c" to 4) on("get size") { it("should return the merged size") { assertThat(subject.size, equalTo(3)) } } on("query whether it contains a key") { it("should query in both maps") { assertTrue { "a" in subject } assertTrue { "c" in subject } assertFalse { "d" in subject } } } on("query whether it contains a value") { it("should query in both maps") { assertTrue { subject.containsValue(1) } assertTrue { subject.containsValue(4) } assertFalse { subject.containsValue(5) } } } on("get a value") { it("should query in both maps") { assertThat(subject["a"], equalTo(1)) assertThat(subject["b"], equalTo(2)) assertThat(subject["c"], equalTo(4)) assertNull(subject["d"]) } } on("query whether it is empty") { it("should query in both maps") { assertFalse { subject.isEmpty() } assertFalse { MergedMap(mutableMapOf("a" to 1), mutableMapOf()).isEmpty() } assertFalse { MergedMap(mutableMapOf(), mutableMapOf("a" to 1)).isEmpty() } assertTrue { MergedMap(mutableMapOf(), mutableMapOf()).isEmpty() } } } on("get entries") { it("should return entries in both maps") { assertThat(subject.entries, equalTo(mergedMap.entries)) } } on("get keys") { it("should return keys in both maps") { assertThat(subject.keys, equalTo(mergedMap.keys)) } } on("get values") { it("should return values in both maps") { assertThat(subject.values.toList(), equalTo(mergedMap.values.toList())) } } on("clear") { subject.clear() it("should clear both maps") { assertTrue { subject.isEmpty() } assertTrue { subject.facade.isEmpty() } assertTrue { subject.fallback.isEmpty() } } } on("put new KV pair") { subject["d"] = 5 it("should put it to the facade map") { assertThat(subject["d"], equalTo(5)) assertThat(subject.facade["d"], equalTo(5)) assertNull(subject.fallback["d"]) } } on("put new KV pairs") { subject.putAll(mapOf("d" to 5, "e" to 6)) it("should put them to the facade map") { assertThat(subject["d"], equalTo(5)) assertThat(subject["e"], equalTo(6)) assertThat(subject.facade["d"], equalTo(5)) assertThat(subject.facade["e"], equalTo(6)) assertNull(subject.fallback["d"]) assertNull(subject.fallback["e"]) } } on("remove key") { it("should remove the key from facade map if it contains the key") { subject.remove("a") assertFalse { "a" in subject } assertFalse { "a" in subject.facade } } it("should remove the key from fallback map if it contains the key") { subject.remove("c") assertFalse { "c" in subject } assertFalse { "c" in subject.fallback } } it("should remove the key from both maps if both of them contain the key") { subject.remove("b") assertFalse { "b" in subject } assertFalse { "b" in subject.facade } assertFalse { "b" in subject.fallback } } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/MultiLayerConfigSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.sameInstance import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.LoadException import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object MultiLayerConfigSpec : SubjectSpek({ subject { Config { addSpec(NetworkBuffer) }.withLayer("multi-layer") } itBehavesLike(ConfigTestSpec) group("multi-layer config") { it("should have specified name") { assertThat(subject.name, equalTo("multi-layer")) } it("should contain same items with parent config") { assertThat( subject[NetworkBuffer.name], equalTo(subject.parent!![NetworkBuffer.name]) ) assertThat( subject[NetworkBuffer.type], equalTo(subject.parent!![NetworkBuffer.type]) ) assertThat( subject[NetworkBuffer.offset], equalTo(subject.parent!![NetworkBuffer.offset]) ) } on("set with item") { subject[NetworkBuffer.name] = "newName" it( "should contain the specified value in the top level," + " and keep the rest levels unchanged" ) { assertThat(subject[NetworkBuffer.name], equalTo("newName")) assertThat(subject.parent!![NetworkBuffer.name], equalTo("buffer")) } } on("set with name") { subject[subject.nameOf(NetworkBuffer.name)] = "newName" it( "should contain the specified value in the top level," + " and keep the rest levels unchanged" ) { assertThat(subject[NetworkBuffer.name], equalTo("newName")) assertThat(subject.parent!![NetworkBuffer.name], equalTo("buffer")) } } on("set parent's value") { subject.parent!![NetworkBuffer.name] = "newName" it("should contain the specified value in both top and parent level") { assertThat(subject[NetworkBuffer.name], equalTo("newName")) assertThat(subject.parent!![NetworkBuffer.name], equalTo("newName")) } } on("add spec") { val spec = object : ConfigSpec(NetworkBuffer.prefix) { val minSize by optional(1) } subject.addSpec(spec) it("should contain items in new spec, and keep the rest level unchanged") { assertThat(spec.minSize in subject, equalTo(true)) assertThat(subject.nameOf(spec.minSize) in subject, equalTo(true)) assertThat(spec.minSize !in subject.parent!!, equalTo(true)) assertThat(subject.nameOf(spec.minSize) !in subject.parent!!, equalTo(true)) } } on("add spec to parent") { val spec = object : ConfigSpec(NetworkBuffer.prefix) { @Suppress("unused") val minSize by optional(1) } it("should throw LayerFrozenException") { assertThat({ subject.parent!!.addSpec(spec) }, throws()) } } on("add item to parent") { val minSize by Spec.dummy.optional(1) it("should throw LayerFrozenException") { assertThat({ subject.parent!!.addItem(minSize) }, throws()) } } on("iterate items in config after adding spec") { val spec = object : ConfigSpec(NetworkBuffer.prefix) { @Suppress("unused") val minSize by optional(1) } subject.addSpec(spec) it("should cover all items in config") { assertThat( subject.iterator().asSequence().toSet(), equalTo((NetworkBuffer.items + spec.items).toSet()) ) } } on("add custom deserializer to mapper in parent") { it("should throw LoadException before adding deserializer") { val spec = object : ConfigSpec() { @Suppress("unused") val item by required() } val parent = Config { addSpec(spec) } val child = parent.withLayer("child") assertThat(parent.mapper, sameInstance(child.mapper)) assertThat( { child.from.map.kv(mapOf("item" to "string")) }, throws() ) } it("should be able to use the specified deserializer after adding") { val spec = object : ConfigSpec() { val item by required() } val parent = Config { addSpec(spec) } val child = parent.withLayer("child") assertThat(parent.mapper, sameInstance(child.mapper)) parent.mapper.registerModule( SimpleModule().apply { addDeserializer(StringWrapper::class.java, StringWrapperDeserializer()) } ) val afterLoad = child.from.map.kv(mapOf("item" to "string")) assertThat(child.mapper, sameInstance(afterLoad.mapper)) assertThat(afterLoad[spec.item], equalTo(StringWrapper("string"))) } } } }) private data class StringWrapper(val string: String) private class StringWrapperDeserializer : StdDeserializer(StringWrapper::class.java) { override fun deserialize( jp: JsonParser, ctxt: DeserializationContext ): StringWrapper { val node = jp.codec.readTree(jp) return StringWrapper(node.textValue()) } } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/NetworkBuffer.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf class NetworkBuffer { companion object : ConfigSpec("network.buffer") { val size by required(description = "size of buffer in KB") val maxSize by lazy(description = "max size of buffer in KB") { it[size] * 2 } val name by optional("buffer", description = "name of buffer") val type by optional( Type.OFF_HEAP, description = """ | type of network buffer. | two type: | - on-heap | - off-heap | buffer is off-heap by default. """.trimMargin("| ") ) val offset by optional(null, description = "initial offset of buffer") } enum class Type { ON_HEAP, OFF_HEAP } } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/ParseDurationSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.ParseException import com.uchuhimo.konf.source.toDuration import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import java.time.Duration object ParseDurationSpec : Spek({ on("parse empty string") { it("throws ParseException") { assertThat({ "".toDuration() }, throws()) } } on("parse string without unit") { it("parse as milliseconds") { assertThat("1".toDuration(), equalTo(Duration.ofMillis(1))) } } on("parse string with unit 'ms'") { it("parse as milliseconds") { assertThat("1ms".toDuration(), equalTo(Duration.ofMillis(1))) } } on("parse string with unit 'millis'") { it("parse as milliseconds") { assertThat("1 millis".toDuration(), equalTo(Duration.ofMillis(1))) } } on("parse string with unit 'milliseconds'") { it("parse as milliseconds") { assertThat("1 milliseconds".toDuration(), equalTo(Duration.ofMillis(1))) } } on("parse string with unit 'us'") { it("parse as microseconds") { assertThat("1us".toDuration(), equalTo(Duration.ofNanos(1000))) } } on("parse string with unit 'micros'") { it("parse as microseconds") { assertThat("1 micros".toDuration(), equalTo(Duration.ofNanos(1000))) } } on("parse string with unit 'microseconds'") { it("parse as microseconds") { assertThat("1 microseconds".toDuration(), equalTo(Duration.ofNanos(1000))) } } on("parse string with unit 'ns'") { it("parse as nanoseconds") { assertThat("1ns".toDuration(), equalTo(Duration.ofNanos(1))) } } on("parse string with unit 'nanos'") { it("parse as nanoseconds") { assertThat("1 nanos".toDuration(), equalTo(Duration.ofNanos(1))) } } on("parse string with unit 'nanoseconds'") { it("parse as nanoseconds") { assertThat("1 nanoseconds".toDuration(), equalTo(Duration.ofNanos(1))) } } on("parse string with unit 'd'") { it("parse as days") { assertThat("1d".toDuration(), equalTo(Duration.ofDays(1))) } } on("parse string with unit 'days'") { it("parse as days") { assertThat("1 days".toDuration(), equalTo(Duration.ofDays(1))) } } on("parse string with unit 'h'") { it("parse as hours") { assertThat("1h".toDuration(), equalTo(Duration.ofHours(1))) } } on("parse string with unit 'hours'") { it("parse as hours") { assertThat("1 hours".toDuration(), equalTo(Duration.ofHours(1))) } } on("parse string with unit 's'") { it("parse as seconds") { assertThat("1s".toDuration(), equalTo(Duration.ofSeconds(1))) } } on("parse string with unit 'seconds'") { it("parse as seconds") { assertThat("1 seconds".toDuration(), equalTo(Duration.ofSeconds(1))) } } on("parse string with unit 'm'") { it("parse as minutes") { assertThat("1m".toDuration(), equalTo(Duration.ofMinutes(1))) } } on("parse string with unit 'minutes'") { it("parse as minutes") { assertThat("1 minutes".toDuration(), equalTo(Duration.ofMinutes(1))) } } on("parse string with float number") { it("parse and convert from double to long") { assertThat("1.5ms".toDuration(), equalTo(Duration.ofNanos(1_500_000))) } } on("parse string with invalid unit") { it("throws ParseException") { assertThat({ "1x".toDuration() }, throws()) } } on("parse string with invalid number") { it("throws ParseException") { assertThat({ "*1s".toDuration() }, throws()) } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/RelocatedConfigSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.sameInstance import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object RelocatedConfigSpec : SubjectSpek({ subject { Prefix("network.buffer") + Config { addSpec(NetworkBuffer) }.at("network.buffer") } configTestSpec() }) object RollUpConfigSpec : SubjectSpek({ subject { Prefix("prefix") + Config { addSpec(NetworkBuffer) } } configTestSpec("prefix.network.buffer") on("prefix is empty string") { it("should return itself") { assertThat(Prefix() + subject, sameInstance(subject)) } } }) object MultiLayerRollUpConfigSpec : SubjectSpek({ subject { (Prefix("prefix") + Config { addSpec(NetworkBuffer) }).withLayer("multi-layer") } configTestSpec("prefix.network.buffer") }) object RollUpMultiLayerConfigSpec : SubjectSpek({ subject { Prefix("prefix") + Config { addSpec(NetworkBuffer) }.withLayer("multi-layer") } configTestSpec("prefix.network.buffer") }) object DrillDownConfigSpec : SubjectSpek({ subject { Config { addSpec(NetworkBuffer) }.at("network") } configTestSpec("buffer") on("path is empty string") { it("should return itself") { assertThat(subject.at(""), sameInstance(subject)) } } }) object MultiLayerDrillDownConfigSpec : SubjectSpek({ subject { Config { addSpec(NetworkBuffer) }.at("network").withLayer("multi-layer") } configTestSpec("buffer") }) object DrillDownMultiLayerConfigSpec : SubjectSpek({ subject { Config { addSpec(NetworkBuffer) }.withLayer("multi-layer").at("network") } configTestSpec("buffer") }) object MultiLayerFacadeDrillDownConfigSpec : SubjectSpek({ subject { ( Config() + Config { addSpec(NetworkBuffer) }.withLayer("layer1") .at("network") .withLayer("layer2") ).withLayer("layer3") } configTestSpec("buffer") }) object MultiLayerRollUpFallbackConfigSpec : SubjectSpek({ subject { ( ( Prefix("prefix") + Config { addSpec(NetworkBuffer) }.withLayer("layer1") ).withLayer("layer2") + Config() ).withLayer("layer3") } configTestSpec("prefix.network.buffer") }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/SizeInBytesSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.ParseException import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on object SizeInBytesSpec : Spek({ on("parse valid string") { it("parse as valid size in bytes") { assertThat(SizeInBytes.parse("1k").bytes, equalTo(1024L)) } } on("init with negative number") { it("should throw IllegalArgumentException") { assertThat({ SizeInBytes(-1L) }, throws()) } } on("parse string without number part") { it("should throw ParseException") { assertThat({ SizeInBytes.parse("m") }, throws()) } } on("parse string with float number") { it("parse and convert from double to long") { assertThat(SizeInBytes.parse("1.5kB").bytes, equalTo(1500L)) } } on("parse string with invalid unit") { it("throws ParseException") { assertThat({ SizeInBytes.parse("1kb") }, throws()) } } on("parse string with invalid number") { it("throws ParseException") { assertThat({ SizeInBytes.parse("*1k") }, throws()) } } on("parse number out of range for a 64-bit long") { it("throws ParseException") { assertThat({ SizeInBytes.parse("1z") }, throws()) } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/TreeNodeSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.has import com.natpryce.hamkrest.sameInstance import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.asSource import com.uchuhimo.konf.source.asTree import com.uchuhimo.konf.source.base.toHierarchical import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object TreeNodeSpec : SubjectSpek({ subject { ContainerNode( mutableMapOf( "level1" to ContainerNode( mutableMapOf("level2" to EmptyNode) ) ) ) } given("a tree node") { on("convert to tree") { it("should return itself") { assertThat(subject.asTree(), sameInstance(subject)) } } on("convert to source") { it("should be the tree in the source") { assertThat(subject.asSource().tree, sameInstance(subject)) } } on("set with an invalid path") { it("should throw InvalidPathException") { assertThat( { subject[""] = EmptyNode }, throws(has(PathConflictException::path, equalTo(""))) ) assertThat( { subject["level1.level2.level3"] = EmptyNode }, throws(has(PathConflictException::path, equalTo("level1.level2.level3"))) ) } } on("minus itself") { it("should return an empty node") { assertThat(subject - subject, equalTo(EmptyNode)) } } on("minus a leaf node") { it("should return an empty node") { assertThat(subject - EmptyNode, equalTo(EmptyNode)) } } on("merge two trees") { val facadeNode = 1.asTree() val facade = mapOf( "key1" to facadeNode, "key2" to EmptyNode, "key4" to mapOf("level2" to facadeNode) ).asTree() val fallbackNode = 2.asTree() val fallback = mapOf( "key1" to EmptyNode, "key2" to fallbackNode, "key3" to fallbackNode, "key4" to mapOf("level2" to fallbackNode) ).asTree() it("should return the merged tree when valid") { assertThat( (fallback + facade).toHierarchical(), equalTo( mapOf( "key1" to facadeNode, "key2" to EmptyNode, "key3" to fallbackNode, "key4" to mapOf("level2" to facadeNode) ).asTree().toHierarchical() ) ) assertThat( facade.withFallback(fallback).toHierarchical(), equalTo( mapOf( "key1" to facadeNode, "key2" to EmptyNode, "key3" to fallbackNode, "key4" to mapOf("level2" to facadeNode) ).asTree().toHierarchical() ) ) assertThat( (EmptyNode + facade).toHierarchical(), equalTo(facade.toHierarchical()) ) assertThat( (fallback + EmptyNode).toHierarchical(), equalTo(EmptyNode.toHierarchical()) ) assertThat( (fallback + mapOf("key1" to mapOf("key2" to EmptyNode)).asTree()).toHierarchical(), equalTo( mapOf( "key1" to mapOf( "key2" to EmptyNode ), "key2" to fallbackNode, "key3" to fallbackNode, "key4" to mapOf("level2" to fallbackNode) ).asTree().toHierarchical() ) ) } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/CustomDeserializerSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek @JsonDeserialize(using = SealedClassDeserializer::class) sealed class SealedClass data class VariantA(val int: Int) : SealedClass() data class VariantB(val double: Double) : SealedClass() class SealedClassDeserializer : StdDeserializer(SealedClass::class.java) { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): SealedClass { val node: JsonNode = p.codec.readTree(p) return if (node.has("int")) { VariantA(node.get("int").asInt()) } else { VariantB(node.get("double").asDouble()) } } } object CustomDeserializerConfig : ConfigSpec("level1.level2") { val variantA by required() val variantB by required() } object CustomDeserializerSpec : SubjectSpek({ subject { Config { addSpec(CustomDeserializerConfig) }.from.map.kv(loadContent) } given("a source") { on("load the source into config") { it("should contain every value specified in the source") { val variantA = VariantA(1) val variantB = VariantB(2.0) assertThat(subject[CustomDeserializerConfig.variantA], equalTo(variantA)) assertThat(subject[CustomDeserializerConfig.variantB], equalTo(variantB)) } } } }) private val loadContent = mapOf( "variantA" to mapOf("int" to 1), "variantB" to mapOf("double" to 2.0) ).mapKeys { (key, _) -> "level1.level2.$key" } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/DefaultLoadersSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.sameInstance import com.natpryce.hamkrest.throws import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.properties.PropertiesProvider import com.uchuhimo.konf.tempFileOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike import spark.Service import java.net.URL import java.util.UUID import java.util.concurrent.TimeUnit object DefaultLoadersSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig) }.from } val item = DefaultLoadersConfig.type given("a loader") { on("load from environment-like map") { val config = subject.envMap(mapOf("SOURCE_TEST_TYPE" to "env")) it("should return a config which contains value from environment-like map") { assertThat(config[item], equalTo("env")) } } on("load from system environment") { val config = subject.env() it("should return a config which contains value from system environment") { assertThat(config[item], equalTo("env")) } } on("load from system properties") { System.setProperty(DefaultLoadersConfig.qualify(DefaultLoadersConfig.type), "system") val config = subject.systemProperties() it("should return a config which contains value from system properties") { assertThat(config[item], equalTo("system")) } } on("dispatch loader based on extension") { it("should throw UnsupportedExtensionException when the extension is unsupported") { assertThat({ subject.dispatchExtension("txt") }, throws()) } it("should return the corresponding loader when the extension is registered") { val extension = UUID.randomUUID().toString() Provider.registerExtension(extension, PropertiesProvider) subject.dispatchExtension(extension) Provider.unregisterExtension(extension) } } on("load from provider") { val config = subject.source(PropertiesProvider).file(tempFileOf(propertiesContent, suffix = ".properties")) it("should load with the provider") { assertThat(config[item], equalTo("properties")) } it("should build a new layer on the parent config") { assertThat(config.parent!!, sameInstance(subject.config)) } } on("load from URL") { val service = Service.ignite() service.port(0) service.get("/source.properties") { _, _ -> propertiesContent } service.awaitInitialization() val config = subject.url(URL("http://localhost:${service.port()}/source.properties")) it("should load as auto-detected URL format") { assertThat(config[item], equalTo("properties")) } service.stop() } on("load from URL string") { val service = Service.ignite() service.port(0) service.get("/source.properties") { _, _ -> propertiesContent } service.awaitInitialization() val config = subject.url("http://localhost:${service.port()}/source.properties") it("should load as auto-detected URL format") { assertThat(config[item], equalTo("properties")) } service.stop() } on("load from file") { val config = subject.file(tempFileOf(propertiesContent, suffix = ".properties")) it("should load as auto-detected file format") { assertThat(config[item], equalTo("properties")) } } on("load from file path") { val file = tempFileOf(propertiesContent, suffix = ".properties") val config = subject.file(file.path) it("should load as auto-detected file format") { assertThat(config[item], equalTo("properties")) } } on("load from watched file") { val file = tempFileOf(propertiesContent, suffix = ".properties") val config = subject.watchFile(file, 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential) val originalValue = config[item] file.writeText(propertiesContent.replace("properties", "newValue")) runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[item] it("should load as auto-detected file format") { assertThat(originalValue, equalTo("properties")) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file with default delay time") { val file = tempFileOf(propertiesContent, suffix = ".properties") val config = subject.watchFile(file, context = Dispatchers.Sequential) val originalValue = config[item] file.writeText(propertiesContent.replace("properties", "newValue")) runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(5)) } val newValue = config[item] it("should load as auto-detected file format") { assertThat(originalValue, equalTo("properties")) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file with listener") { val file = tempFileOf(propertiesContent, suffix = ".properties") var newValue = "" subject.watchFile( file, 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential ) { config, _ -> newValue = config[item] } file.writeText(propertiesContent.replace("properties", "newValue")) runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file path") { val file = tempFileOf(propertiesContent, suffix = ".properties") val config = subject.watchFile(file.path, 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential) val originalValue = config[item] file.writeText(propertiesContent.replace("properties", "newValue")) runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[item] it("should load as auto-detected file format") { assertThat(originalValue, equalTo("properties")) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file path with default delay time") { val file = tempFileOf(propertiesContent, suffix = ".properties") val config = subject.watchFile(file.path, context = Dispatchers.Sequential) val originalValue = config[item] file.writeText(propertiesContent.replace("properties", "newValue")) runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(5)) } val newValue = config[item] it("should load as auto-detected file format") { assertThat(originalValue, equalTo("properties")) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched URL") { var content = propertiesContent val service = Service.ignite() service.port(0) service.get("/source.properties") { _, _ -> content } service.awaitInitialization() val url = "http://localhost:${service.port()}/source.properties" val config = subject.watchUrl(URL(url), period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential) val originalValue = config[item] content = propertiesContent.replace("properties", "newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[item] it("should load as auto-detected URL format") { assertThat(originalValue, equalTo("properties")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched URL with default delay time") { var content = propertiesContent val service = Service.ignite() service.port(0) service.get("/source.properties") { _, _ -> content } service.awaitInitialization() val url = "http://localhost:${service.port()}/source.properties" val config = subject.watchUrl(URL(url), context = Dispatchers.Sequential) val originalValue = config[item] content = propertiesContent.replace("properties", "newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(5)) } val newValue = config[item] it("should load as auto-detected URL format") { assertThat(originalValue, equalTo("properties")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched URL string") { var content = propertiesContent val service = Service.ignite() service.port(0) service.get("/source.properties") { _, _ -> content } service.awaitInitialization() val url = "http://localhost:${service.port()}/source.properties" val config = subject.watchUrl(url, period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential) val originalValue = config[item] content = propertiesContent.replace("properties", "newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[item] it("should load as auto-detected URL format") { assertThat(originalValue, equalTo("properties")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched URL string with default delay time") { var content = propertiesContent val service = Service.ignite() service.port(0) service.get("/source.properties") { _, _ -> content } service.awaitInitialization() val url = "http://localhost:${service.port()}/source.properties" val config = subject.watchUrl(url, context = Dispatchers.Sequential) val originalValue = config[item] content = propertiesContent.replace("properties", "newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(5)) } val newValue = config[item] it("should load as auto-detected URL format") { assertThat(originalValue, equalTo("properties")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched URL string with listener") { var content = propertiesContent val service = Service.ignite() service.port(0) service.get("/source.properties") { _, _ -> content } service.awaitInitialization() val url = "http://localhost:${service.port()}/source.properties" var newValue = "" val config = subject.watchUrl( url, period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential ) { config, _ -> newValue = config[item] } val originalValue = config[item] content = propertiesContent.replace("properties", "newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } it("should load as auto-detected URL format") { assertThat(originalValue, equalTo("properties")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from map") { it("should use the same config") { assertThat(subject.config, sameInstance(subject.map.config)) } } } }) object DefaultLoadersWithFlattenEnvSpec : Spek({ given("a loader") { on("load as flatten format from system environment") { val config = Config { addSpec(FlattenDefaultLoadersConfig) }.from.env(nested = false) it("should return a config which contains value from system environment") { assertThat(config[FlattenDefaultLoadersConfig.SOURCE_TEST_TYPE], equalTo("env")) } } } }) object MappedDefaultLoadersSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig["source"]) }.from.mapped { it["source"] } } itBehavesLike(DefaultLoadersSpec) }) object PrefixedDefaultLoadersSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig.withPrefix("prefix")) }.from.prefixed("prefix") } itBehavesLike(DefaultLoadersSpec) }) object ScopedDefaultLoadersSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig["source"]) }.from.scoped("source") } itBehavesLike(DefaultLoadersSpec) }) object FlattenDefaultLoadersConfig : ConfigSpec("") { val SOURCE_TEST_TYPE by required() } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/DefaultProvidersSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.sameInstance import com.natpryce.hamkrest.throws import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.properties.PropertiesProvider import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import spark.Service import java.net.URL import java.util.UUID object DefaultProvidersSpec : SubjectSpek({ subject { Source.from } val item = DefaultLoadersConfig.type given("a provider") { on("provide source from system environment") { val config = subject.env().toConfig() it("should return a source which contains value from system environment") { assertThat(config[item], equalTo("env")) } } on("provide flatten source from system environment") { val config = subject.env(nested = false).toFlattenConfig() it("should return a source which contains value from system environment") { assertThat(config[FlattenDefaultLoadersConfig.SOURCE_TEST_TYPE], equalTo("env")) } } on("provide source from system properties") { System.setProperty(DefaultLoadersConfig.qualify(DefaultLoadersConfig.type), "system") val config = subject.systemProperties().toConfig() it("should return a source which contains value from system properties") { assertThat(config[item], equalTo("system")) } } on("dispatch provider based on extension") { it("should throw UnsupportedExtensionException when the extension is unsupported") { assertThat({ subject.dispatchExtension("txt") }, throws()) } it("should return the corresponding provider when the extension is registered") { val extension = UUID.randomUUID().toString() Provider.registerExtension(extension, PropertiesProvider) assertThat(subject.dispatchExtension(extension), sameInstance(PropertiesProvider as Provider)) Provider.unregisterExtension(extension) } } on("provide source from URL") { val service = Service.ignite() service.port(0) service.get("/source.properties") { _, _ -> propertiesContent } service.awaitInitialization() val config = subject.url(URL("http://localhost:${service.port()}/source.properties")).toConfig() it("should provide as auto-detected URL format") { assertThat(config[item], equalTo("properties")) } service.stop() } on("provide source from URL string") { val service = Service.ignite() service.port(0) service.get("/source.properties") { _, _ -> propertiesContent } service.awaitInitialization() val config = subject.url("http://localhost:${service.port()}/source.properties").toConfig() it("should provide as auto-detected URL format") { assertThat(config[item], equalTo("properties")) } service.stop() } on("provide source from file") { val config = subject.file(tempFileOf(propertiesContent, suffix = ".properties")).toConfig() it("should provide as auto-detected file format") { assertThat(config[item], equalTo("properties")) } } on("provide source from file path") { val file = tempFileOf(propertiesContent, suffix = ".properties") val config = subject.file(file.path).toConfig() it("should provide as auto-detected file format") { assertThat(config[item], equalTo("properties")) } } } }) fun Source.toFlattenConfig(): Config = Config { addSpec(FlattenDefaultLoadersConfig) }.withSource(this) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/FacadeSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.name import com.uchuhimo.konf.source.base.asKVSource import com.uchuhimo.konf.toPath import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import kotlin.test.assertFalse import kotlin.test.assertTrue object FacadeSourceSpec : Spek({ given("a source with facade") { it("contains facade & fallback info") { val facadeSource = 1.asSource() val fallbackSource = 2.asSource() val source = fallbackSource + facadeSource assertThat(source.info["facade"], equalTo(facadeSource.description)) assertThat(source.info["fallback"], equalTo(fallbackSource.description)) } on("path/key is in facade source") { val path = listOf("a", "b") val key = path.name val fallbackSource = mapOf(key to "fallback").asKVSource() val facadeSource = mapOf(key to "facade").asKVSource() val source = fallbackSource + facadeSource it("gets value from facade source") { assertTrue(path in source) assertTrue(key in source) assertThat(source[path].asValue(), equalTo(facadeSource[path].asValue())) assertThat(source[key].asValue(), equalTo(facadeSource[key].asValue())) assertThat( source.getOrNull(path)?.asValue(), equalTo(facadeSource.getOrNull(path)?.asValue()) ) assertThat( source.getOrNull(key)?.asValue(), equalTo(facadeSource.getOrNull(key)?.asValue()) ) } } on("path/key is in fallback source") { val path = listOf("a", "b") val key = path.name val fallbackSource = mapOf(key to "fallback").asKVSource() val facadePath = listOf("a", "c") val facadeKey = facadePath.name val facadeSource = mapOf(facadeKey to "facade").asKVSource() val source = fallbackSource + facadeSource it("gets value from fallback source") { assertTrue(path in source) assertTrue(key in source) assertThat(source[path].asValue(), equalTo(fallbackSource[path].asValue())) assertThat(source[key].asValue(), equalTo(fallbackSource[key].asValue())) assertThat( source.getOrNull(path)?.asValue(), equalTo(fallbackSource.getOrNull(path)?.asValue()) ) assertThat( source.getOrNull(key)?.asValue(), equalTo(fallbackSource.getOrNull(key)?.asValue()) ) } it("contains value in facade source") { assertTrue(facadePath in source) assertTrue(facadeKey in source) } it("does not contain value which is not existed in both facade source and fallback source") { assertFalse("a.d".toPath() in source) assertFalse("a.d" in source) } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/FallbackSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.name import com.uchuhimo.konf.source.base.asKVSource import com.uchuhimo.konf.toPath import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import kotlin.test.assertFalse import kotlin.test.assertTrue object FallbackSourceSpec : Spek({ given("a source with fallback") { it("contains facade & fallback info") { val facadeSource = 1.asSource() val fallbackSource = 2.asSource() val source = facadeSource.withFallback(fallbackSource) assertThat(source.info["facade"], equalTo(facadeSource.description)) assertThat(source.info["fallback"], equalTo(fallbackSource.description)) } on("path/key is in facade source") { val path = listOf("a", "b") val key = path.name val fallbackSource = mapOf(key to "fallback").asKVSource() val facadeSource = mapOf(key to "facade").asKVSource() val source = facadeSource.withFallback(fallbackSource) it("gets value from facade source") { assertTrue(path in source) assertTrue(key in source) assertThat(source[path].asValue(), equalTo(facadeSource[path].asValue())) assertThat(source[key].asValue(), equalTo(facadeSource[key].asValue())) assertThat( source.getOrNull(path)?.asValue(), equalTo(facadeSource.getOrNull(path)?.asValue()) ) assertThat( source.getOrNull(key)?.asValue(), equalTo(facadeSource.getOrNull(key)?.asValue()) ) } } on("path/key is in fallback source") { val path = listOf("a", "b") val key = path.name val fallbackSource = mapOf(key to "fallback").asKVSource() val facadePath = listOf("a", "c") val facadeKey = facadePath.name val facadeSource = mapOf(facadeKey to "facade").asKVSource() val source = facadeSource.withFallback(fallbackSource) it("gets value from fallback source") { assertTrue(path in source) assertTrue(key in source) assertThat(source[path].asValue(), equalTo(fallbackSource[path].asValue())) assertThat(source[key].asValue(), equalTo(fallbackSource[key].asValue())) assertThat( source.getOrNull(path)?.asValue(), equalTo(fallbackSource.getOrNull(path)?.asValue()) ) assertThat( source.getOrNull(key)?.asValue(), equalTo(fallbackSource.getOrNull(key)?.asValue()) ) } it("contains value in facade source") { assertTrue(facadePath in source) assertTrue(facadeKey in source) } it("does not contain value which is not existed in both facade source and fallback source") { assertFalse("a.d".toPath() in source) assertFalse("a.d" in source) } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/LoaderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.throws import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.tempFileOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import spark.Service import java.util.concurrent.TimeUnit object LoaderSpec : SubjectSpek({ val parentConfig = Config { addSpec(SourceType) } subject { parentConfig.from.properties } given("a loader") { it("should fork from parent config") { assertThat(subject.config, equalTo(parentConfig)) } on("load from reader") { val config = subject.reader("type = reader".reader()) it("should return a config which contains value from reader") { assertThat(config[SourceType.type], equalTo("reader")) } } on("load from input stream") { val config = subject.inputStream( tempFileOf("type = inputStream").inputStream() ) it("should return a config which contains value from input stream") { assertThat(config[SourceType.type], equalTo("inputStream")) } } on("load from file") { val config = subject.file(tempFileOf("type = file")) it("should return a config which contains value in file") { assertThat(config[SourceType.type], equalTo("file")) } } on("load from file path") { val config = subject.file(tempFileOf("type = file").toString()) it("should return a config which contains value in file") { assertThat(config[SourceType.type], equalTo("file")) } } on("load from watched file") { val file = tempFileOf("type = originalValue") val config = subject.watchFile(file, delayTime = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential) val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[SourceType.type] it("should return a config which contains value in file") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file on macOS") { val os = System.getProperty("os.name") System.setProperty("os.name", "mac") val file = tempFileOf("type = originalValue") val config = subject.watchFile(file, delayTime = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential) val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[SourceType.type] it("should return a config which contains value in file") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } System.setProperty("os.name", os) } on("load from watched file with default delay time") { val file = tempFileOf("type = originalValue") val config = subject.watchFile(file, context = Dispatchers.Sequential) val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(5)) } val newValue = config[SourceType.type] it("should return a config which contains value in file") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file with listener") { val file = tempFileOf("type = originalValue") var newValue = "" subject.watchFile( file, delayTime = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential ) { config, _ -> newValue = config[SourceType.type] } file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file path") { val file = tempFileOf("type = originalValue") val config = subject.watchFile(file.toString(), delayTime = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential) val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[SourceType.type] it("should return a config which contains value in file") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file path with default delay time") { val file = tempFileOf("type = originalValue") val config = subject.watchFile(file.toString(), context = Dispatchers.Sequential) val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(5)) } val newValue = config[SourceType.type] it("should return a config which contains value in file") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value when file has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from string") { val config = subject.string("type = string") it("should return a config which contains value in string") { assertThat(config[SourceType.type], equalTo("string")) } } on("load from byte array") { val config = subject.bytes("type = bytes".toByteArray()) it("should return a config which contains value in byte array") { assertThat(config[SourceType.type], equalTo("bytes")) } } on("load from byte array slice") { val config = subject.bytes("|type = slice|".toByteArray(), 1, 12) it("should return a config which contains value in byte array slice") { assertThat(config[SourceType.type], equalTo("slice")) } } on("load from HTTP URL") { val service = Service.ignite() service.port(0) service.get("/source") { _, _ -> "type = http" } service.awaitInitialization() val config = subject.url("http://localhost:${service.port()}/source") it("should return a config which contains value in URL") { assertThat(config[SourceType.type], equalTo("http")) } service.stop() } on("load from file URL") { val file = tempFileOf("type = fileUrl") val config = subject.url(file.toURI().toURL()) it("should return a config which contains value in URL") { assertThat(config[SourceType.type], equalTo("fileUrl")) } } on("load from file URL string") { val url = tempFileOf("type = fileUrl").toURI().toURL().toString() val config = subject.url(url) it("should return a config which contains value in URL") { assertThat(config[SourceType.type], equalTo("fileUrl")) } } on("load from watched HTTP URL") { var content = "type = originalValue" val service = Service.ignite() service.port(0) service.get("/source") { _, _ -> content } service.awaitInitialization() val url = "http://localhost:${service.port()}/source" val config = subject.watchUrl(url, context = Dispatchers.Sequential) val originalValue = config[SourceType.type] content = "type = newValue" runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(5)) } val newValue = config[SourceType.type] it("should return a config which contains value in URL") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file URL") { val file = tempFileOf("type = originalValue") val config = subject.watchUrl(file.toURI().toURL(), period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential) val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[SourceType.type] it("should return a config which contains value in URL") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file URL with default delay time") { val file = tempFileOf("type = originalValue") val config = subject.watchUrl(file.toURI().toURL(), context = Dispatchers.Sequential) val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(5)) } val newValue = config[SourceType.type] it("should return a config which contains value in URL") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file URL with listener") { val file = tempFileOf("type = originalValue") var newValue = "" val config = subject.watchUrl( file.toURI().toURL(), period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential ) { config, _ -> newValue = config[SourceType.type] } val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } it("should return a config which contains value in URL") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file URL string") { val file = tempFileOf("type = originalValue") val url = file.toURI().toURL() val config = subject.watchUrl(url.toString(), period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential) val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[SourceType.type] it("should return a config which contains value in URL") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from watched file URL string with default delay time") { val file = tempFileOf("type = originalValue") val url = file.toURI().toURL() val config = subject.watchUrl(url.toString(), context = Dispatchers.Sequential) val originalValue = config[SourceType.type] file.writeText("type = newValue") runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(5)) } val newValue = config[SourceType.type] it("should return a config which contains value in URL") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value after URL content has been changed") { assertThat(newValue, equalTo("newValue")) } } on("load from resource") { val config = subject.resource("source/provider.properties") it("should return a config which contains value in resource") { assertThat(config[SourceType.type], equalTo("resource")) } } on("load from non-existed resource") { it("should throw SourceNotFoundException") { assertThat( { subject.resource("source/no-provider.properties") }, throws() ) } } } }) private object SourceType : ConfigSpec("") { val type by required() } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/MergedSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.base.toHierarchicalMap import com.uchuhimo.konf.toSizeInBytes import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike import java.math.BigDecimal import java.math.BigInteger import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.OffsetDateTime import java.time.OffsetTime import java.time.Year import java.time.YearMonth import java.time.ZonedDateTime import java.util.Date object MergedSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) enable(Feature.FAIL_ON_UNKNOWN_PATH) }.withSource(fallbackContent.asSource() + facadeContent.asSource()) } itBehavesLike(SourceLoadBaseSpec) }) object MergedSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) }.withSource(fallbackContent.asSource() + facadeContent.asSource()) Config { addSpec(ConfigForLoad) }.from.map.hierarchical(config.toHierarchicalMap()) } itBehavesLike(SourceLoadBaseSpec) }) private val facadeContent = mapOf( "level1" to mapOf( "level2" to mapOf( "empty" to "null", "literalEmpty" to "null", "present" to 1, "boolean" to false, "double" to 1.5, "float" to -1.5f, "bigDecimal" to BigDecimal.valueOf(1.5), "char" to 'a', "enum" to "LABEL2", "array" to mapOf( "boolean" to listOf(true, false), "byte" to listOf(1, 2, 3), "short" to listOf(1, 2, 3), "int" to listOf(1, 2, 3), "object" to mapOf( "boolean" to listOf(true, false), "int" to listOf(1, 2, 3), "string" to listOf("one", "two", "three") ) ), "list" to listOf(1, 2, 3), "mutableList" to listOf(1, 2, 3), "listOfList" to listOf(listOf(1, 2), listOf(3, 4)), "map" to mapOf( "a" to 1, "c" to 3 ), "intMap" to mapOf( 1 to "a", 3 to "c" ), "sortedMap" to mapOf( "c" to 3, "a" to 1 ), "listOfMap" to listOf( mapOf("a" to 1, "b" to 2), mapOf("a" to 3, "b" to 4) ), "nested" to listOf(listOf(listOf(mapOf("a" to 1)))), "pair" to mapOf("first" to 1, "second" to 2), "clazz" to mapOf( "boolean" to false, "int" to 1, "short" to 2.toShort(), "byte" to 3.toByte(), "bigInteger" to BigInteger.valueOf(4), "long" to 4L, "char" to 'a', "string" to "string", "offsetTime" to OffsetTime.parse("10:15:30+01:00"), "offsetDateTime" to OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), "zonedDateTime" to ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), "localDate" to LocalDate.parse("2007-12-03"), "localTime" to LocalTime.parse("10:15:30"), "localDateTime" to LocalDateTime.parse("2007-12-03T10:15:30"), "date" to Date.from(Instant.parse("2007-12-03T10:15:30Z")), "year" to Year.parse("2007"), "yearMonth" to YearMonth.parse("2007-12"), "instant" to Instant.parse("2007-12-03T10:15:30.00Z"), "duration" to "P2DT3H4M".toDuration(), "simpleDuration" to "200millis".toDuration(), "size" to "10k".toSizeInBytes(), "enum" to "LABEL2", "booleanArray" to listOf(true, false), "nested" to listOf(listOf(listOf(mapOf("a" to 1)))) ) ) ) ) private val fallbackContent = mapOf( "level1" to mapOf( "level2" to mapOf( "boolean" to true, "int" to 1, "short" to 2.toShort(), "byte" to 3.toByte(), "bigInteger" to BigInteger.valueOf(4), "long" to 4L, "char" to 'b', "string" to "string", "offsetTime" to OffsetTime.parse("10:15:30+01:00"), "offsetDateTime" to OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), "zonedDateTime" to ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), "localDate" to LocalDate.parse("2007-12-03"), "localTime" to LocalTime.parse("10:15:30"), "localDateTime" to LocalDateTime.parse("2007-12-03T10:15:30"), "date" to Date.from(Instant.parse("2007-12-03T10:15:30Z")), "year" to Year.parse("2007"), "yearMonth" to YearMonth.parse("2007-12"), "instant" to Instant.parse("2007-12-03T10:15:30.00Z"), "duration" to "P2DT3H4M".toDuration(), "simpleDuration" to "200millis".toDuration(), "size" to "10k".toSizeInBytes(), "enum" to "LABEL3", "array" to mapOf( "int" to listOf(3, 2, 1), "long" to listOf(4L, 5L, 6L), "float" to listOf(-1.0F, 0.0F, 1.0F), "double" to listOf(-1.0, 0.0, 1.0), "char" to listOf('a', 'b', 'c'), "object" to mapOf( "string" to listOf("three", "two", "one"), "enum" to listOf("LABEL1", "LABEL2", "LABEL3") ) ), "listOfList" to listOf(listOf(1, 2)), "set" to listOf(1, 2, 1), "sortedSet" to listOf(2, 1, 1, 3), "map" to mapOf( "b" to 2, "c" to 3 ), "intMap" to mapOf( 2 to "b", 3 to "c" ), "sortedMap" to mapOf( "b" to 2, "a" to 1 ), "listOfMap" to listOf( mapOf("a" to 1, "b" to 2), mapOf("a" to 3, "b" to 4) ), "nested" to listOf(listOf(listOf(mapOf("a" to 1)))), "pair" to mapOf("first" to 1, "second" to 2), "clazz" to mapOf( "empty" to "null", "literalEmpty" to "null", "present" to 1, "boolean" to true, "double" to 1.5, "float" to -1.5f, "bigDecimal" to BigDecimal.valueOf(1.5), "char" to 'c', "enum" to "LABEL1", "booleanArray" to listOf(true, false), "nested" to listOf(listOf(listOf(mapOf("a" to 1)))) ) ) ) ) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/ProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.properties.PropertiesProvider import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike import org.junit.jupiter.api.assertThrows import spark.Service import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.net.URL import kotlin.test.assertTrue object ProviderSpec : SubjectSpek({ subject { PropertiesProvider } given("a provider") { on("create source from reader") { val source = subject.reader("type = reader".reader()) it("should return a source which contains value from reader") { assertThat(source["type"].asValue(), equalTo("reader")) } } on("create source from input stream") { val source = subject.inputStream( tempFileOf("type = inputStream").inputStream() ) it("should return a source which contains value from input stream") { assertThat(source["type"].asValue(), equalTo("inputStream")) } } on("create source from file") { val file = tempFileOf("type = file") val source = subject.file(file) it("should create from the specified file") { assertThat(source.info["file"], equalTo(file.toString())) } it("should return a source which contains value in file") { assertThat(source["type"].asValue(), equalTo("file")) } it("should not lock the file") { assertTrue { file.delete() } } } on("create source from not-existed file") { it("should throw exception") { assertThrows { subject.file(File("not_existed.json")) } } it("should return an empty source if optional") { assertThat( subject.file(File("not_existed.json"), optional = true).tree.children, equalTo(mutableMapOf()) ) } } on("create source from file path") { val file = tempFileOf("type = file").toString() val source = subject.file(file) it("should create from the specified file path") { assertThat(source.info["file"], equalTo(file)) } it("should return a source which contains value in file") { assertThat(source["type"].asValue(), equalTo("file")) } } on("create source from not-existed file path") { it("should throw exception") { assertThrows { subject.file("not_existed.json") } } it("should return an empty source if optional") { assertThat( subject.file("not_existed.json", optional = true).tree.children, equalTo(mutableMapOf()) ) } } on("create source from string") { val content = "type = string" val source = subject.string(content) it("should create from the specified string") { assertThat(source.info["content"], equalTo("\"\n$content\n\"")) } it("should return a source which contains value in string") { assertThat(source["type"].asValue(), equalTo("string")) } } on("create source from byte array") { val source = subject.bytes("type = bytes".toByteArray()) it("should return a source which contains value in byte array") { assertThat(source["type"].asValue(), equalTo("bytes")) } } on("create source from byte array slice") { val source = subject.bytes("|type = slice|".toByteArray(), 1, 12) it("should return a source which contains value in byte array slice") { assertThat(source["type"].asValue(), equalTo("slice")) } } on("create source from HTTP URL") { val service = Service.ignite() service.port(0) service.get("/source") { _, _ -> "type = http" } service.awaitInitialization() val urlPath = "http://localhost:${service.port()}/source" val source = subject.url(URL(urlPath)) it("should create from the specified URL") { assertThat(source.info["url"], equalTo(urlPath)) } it("should return a source which contains value in URL") { assertThat(source["type"].asValue(), equalTo("http")) } service.stop() } on("create source from not-existed HTTP URL") { it("should throw exception") { assertThrows { subject.url(URL("http://localhost/not_existed.json")) } } it("should return an empty source if optional") { assertThat( subject.url(URL("http://localhost/not_existed.json"), optional = true).tree.children, equalTo(mutableMapOf()) ) } } on("create source from file URL") { val file = tempFileOf("type = fileUrl") val url = file.toURI().toURL() val source = subject.url(url) it("should create from the specified URL") { assertThat(source.info["url"], equalTo(url.toString())) } it("should return a source which contains value in URL") { assertThat(source["type"].asValue(), equalTo("fileUrl")) } it("should not lock the file") { assertTrue { file.delete() } } } on("create source from not-existed file URL") { it("should throw exception") { assertThrows { subject.url(URL("file://localhost/not_existed.json")) } } it("should return an empty source if optional") { assertThat( subject.url(URL("file://localhost/not_existed.json"), optional = true).tree.children, equalTo(mutableMapOf()) ) } } on("create source from file URL string") { val file = tempFileOf("type = fileUrl") val url = file.toURI().toURL().toString() val source = subject.url(url) it("should create from the specified URL string") { assertThat(source.info["url"], equalTo(url)) } it("should return a source which contains value in URL") { assertThat(source["type"].asValue(), equalTo("fileUrl")) } } on("create source from not-existed file URL string") { it("should throw exception") { assertThrows { subject.url("file://localhost/not_existed.json") } } it("should return an empty source if optional") { assertThat( subject.url("file://localhost/not_existed.json", optional = true).tree.children, equalTo(mutableMapOf()) ) } } on("create source from resource") { val resource = "source/provider.properties" val source = subject.resource(resource) it("should create from the specified resource") { assertThat(source.info["resource"], equalTo(resource)) } it("should return a source which contains value in resource") { assertThat(source["type"].asValue(), equalTo("resource")) } } on("create source from non-existed resource") { it("should throw SourceNotFoundException") { assertThat( { subject.resource("source/no-provider.properties") }, throws() ) } it("should return an empty source if optional") { assertThat( subject.resource("source/no-provider.properties", optional = true).tree.children, equalTo(mutableMapOf()) ) } } } }) object MappedProviderSpec : SubjectSpek({ subject { PropertiesProvider.map { source -> source.withPrefix("prefix")["prefix"] } } itBehavesLike(ProviderSpec) }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/SourceInfoSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object SourceInfoSpec : SubjectSpek({ subject { SourceInfo("a" to "1") } given("a source info") { on("use as a map") { it("should behave like a map") { assertThat(subject.toMap(), equalTo(mapOf("a" to "1"))) } } on("with new KV pairs") { it("should contain the new KV pairs") { assertThat( subject.with("b" to "2", "c" to "3").toMap(), equalTo(mapOf("a" to "1", "b" to "2", "c" to "3")) ) } } on("with another source info") { it("should contain the new KV pairs in another source info") { assertThat( subject.with(SourceInfo("b" to "2", "c" to "3")).toMap(), equalTo(mapOf("a" to "1", "b" to "2", "c" to "3")) ) } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/SourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.Config import com.uchuhimo.konf.tempFile import com.uchuhimo.konf.toSizeInBytes import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.math.BigDecimal import java.math.BigInteger import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.OffsetDateTime import java.time.OffsetTime import java.time.Year import java.time.YearMonth import java.time.ZonedDateTime import java.util.Date object SourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) }.from.map.kv(loadContent) } itBehavesLike(SourceLoadBaseSpec) }) object SourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) }.from.map.kv(loadContent) Config { addSpec(ConfigForLoad) }.from.map.kv(config.toMap()) } itBehavesLike(SourceLoadBaseSpec) }) object SourceReloadFromDiskSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) }.from.map.kv(loadContent) val map = config.toMap() val newMap = tempFile().run { ObjectOutputStream(outputStream()).use { it.writeObject(map) } ObjectInputStream(inputStream()).use { @Suppress("UNCHECKED_CAST") it.readObject() as Map } } Config { addSpec(ConfigForLoad) }.from.map.kv(newMap) } itBehavesLike(SourceLoadBaseSpec) }) object KVSourceFromDefaultProvidersSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) }.withSource(Source.from.map.kv(loadContent)) } itBehavesLike(SourceLoadBaseSpec) }) private val loadContent = mapOf( "empty" to "null", "literalEmpty" to "null", "present" to 1, "boolean" to false, "int" to 1, "short" to 2.toShort(), "byte" to 3.toByte(), "bigInteger" to BigInteger.valueOf(4), "long" to 4L, "double" to 1.5, "float" to -1.5f, "bigDecimal" to BigDecimal.valueOf(1.5), "char" to 'a', "string" to "string", "offsetTime" to OffsetTime.parse("10:15:30+01:00"), "offsetDateTime" to OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), "zonedDateTime" to ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), "localDate" to LocalDate.parse("2007-12-03"), "localTime" to LocalTime.parse("10:15:30"), "localDateTime" to LocalDateTime.parse("2007-12-03T10:15:30"), "date" to Date.from(Instant.parse("2007-12-03T10:15:30Z")), "year" to Year.parse("2007"), "yearMonth" to YearMonth.parse("2007-12"), "instant" to Instant.parse("2007-12-03T10:15:30.00Z"), "duration" to "P2DT3H4M".toDuration(), "simpleDuration" to "200millis".toDuration(), "size" to "10k".toSizeInBytes(), "enum" to "LABEL2", "array.boolean" to listOf(true, false), "array.byte" to listOf(1, 2, 3), "array.short" to listOf(1, 2, 3), "array.int" to listOf(1, 2, 3), "array.long" to listOf(4L, 5L, 6L), "array.float" to listOf(-1.0F, 0.0F, 1.0F), "array.double" to listOf(-1.0, 0.0, 1.0), "array.char" to listOf('a', 'b', 'c'), "array.object.boolean" to listOf(true, false), "array.object.int" to listOf(1, 2, 3), "array.object.string" to listOf("one", "two", "three"), "array.object.enum" to listOf("LABEL1", "LABEL2", "LABEL3"), "list" to listOf(1, 2, 3), "mutableList" to listOf(1, 2, 3), "listOfList" to listOf(listOf(1, 2), listOf(3, 4)), "set" to listOf(1, 2, 1), "sortedSet" to listOf(2, 1, 1, 3), "map" to mapOf( "a" to 1, "b" to 2, "c" to 3 ), "intMap" to mapOf( 1 to "a", 2 to "b", 3 to "c" ), "sortedMap" to mapOf( "c" to 3, "b" to 2, "a" to 1 ), "listOfMap" to listOf( mapOf("a" to 1, "b" to 2), mapOf("a" to 3, "b" to 4) ), "nested" to listOf(listOf(listOf(mapOf("a" to 1)))), "pair" to mapOf("first" to 1, "second" to 2), "clazz" to mapOf( "empty" to "null", "literalEmpty" to "null", "present" to 1, "boolean" to false, "int" to 1, "short" to 2.toShort(), "byte" to 3.toByte(), "bigInteger" to BigInteger.valueOf(4), "long" to 4L, "double" to 1.5, "float" to -1.5f, "bigDecimal" to BigDecimal.valueOf(1.5), "char" to 'a', "string" to "string", "offsetTime" to OffsetTime.parse("10:15:30+01:00"), "offsetDateTime" to OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), "zonedDateTime" to ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), "localDate" to LocalDate.parse("2007-12-03"), "localTime" to LocalTime.parse("10:15:30"), "localDateTime" to LocalDateTime.parse("2007-12-03T10:15:30"), "date" to Date.from(Instant.parse("2007-12-03T10:15:30Z")), "year" to Year.parse("2007"), "yearMonth" to YearMonth.parse("2007-12"), "instant" to Instant.parse("2007-12-03T10:15:30.00Z"), "duration" to "P2DT3H4M".toDuration(), "simpleDuration" to "200millis".toDuration(), "size" to "10k".toSizeInBytes(), "enum" to "LABEL2", "booleanArray" to listOf(true, false), "nested" to listOf(listOf(listOf(mapOf("a" to 1)))) ) ).mapKeys { (key, _) -> "level1.level2.$key" } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/SourceNodeSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.EmptyNode import com.uchuhimo.konf.TreeNode import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object ListSourceNodeSpec : SubjectSpek({ subject { ListSourceNode(listOf(EmptyNode, EmptyNode)) } on("get children") { it("should return a map indexed by integer") { assertThat( subject.children, equalTo( mutableMapOf( "0" to EmptyNode, "1" to EmptyNode ) ) ) } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/SourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.absent import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.has import com.natpryce.hamkrest.sameInstance import com.natpryce.hamkrest.throws import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.Feature import com.uchuhimo.konf.NetworkBuffer import com.uchuhimo.konf.Prefix import com.uchuhimo.konf.SizeInBytes import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.name import com.uchuhimo.konf.source.base.ValueSource import com.uchuhimo.konf.source.base.asKVSource import com.uchuhimo.konf.source.base.toHierarchical import com.uchuhimo.konf.toPath import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.junit.jupiter.api.assertThrows import java.math.BigDecimal import java.math.BigInteger import java.time.Duration import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.OffsetDateTime import java.time.OffsetTime import java.time.Year import java.time.YearMonth import java.time.ZoneOffset import java.time.ZonedDateTime import java.util.Date import java.util.concurrent.ConcurrentHashMap import kotlin.test.assertFalse import kotlin.test.assertTrue object SourceSpec : Spek({ given("a source") { group("get operation") { val value: Source = ValueSource(Unit) val tree = value.tree val validPath = "a.b".toPath() val invalidPath = "a.c".toPath() val validKey = "a" val invalidKey = "b" val sourceForPath by memoized { mapOf(validPath.name to value).asKVSource() } val sourceForKey by memoized { mapOf(validKey to value).asSource() } on("find a valid path") { it("should contain the value") { assertTrue(validPath in sourceForPath) } } on("find an invalid path") { it("should not contain the value") { assertTrue(invalidPath !in sourceForPath) } } on("get by a valid path using `getOrNull`") { it("should return the corresponding value") { assertThat(sourceForPath.getOrNull(validPath)?.tree, equalTo(tree)) } } on("get by an invalid path using `getOrNull`") { it("should return null") { assertThat(sourceForPath.getOrNull(invalidPath), absent()) } } on("get by a valid path using `get`") { it("should return the corresponding value") { assertThat(sourceForPath[validPath].tree, equalTo(tree)) } } on("get by an invalid path using `get`") { it("should throw NoSuchPathException") { assertThat( { sourceForPath[invalidPath] }, throws(has(NoSuchPathException::path, equalTo(invalidPath))) ) } } on("find a valid key") { it("should contain the value") { assertTrue(validKey in sourceForKey) } } on("find an invalid key") { it("should not contain the value") { assertTrue(invalidKey !in sourceForKey) } } on("get by a valid key using `getOrNull`") { it("should return the corresponding value") { assertThat(sourceForKey.getOrNull(validKey)?.tree, equalTo(tree)) } } on("get by an invalid key using `getOrNull`") { it("should return null") { assertThat(sourceForKey.getOrNull(invalidKey), absent()) } } on("get by a valid key using `get`") { it("should return the corresponding value") { assertThat(sourceForKey[validKey].tree, equalTo(tree)) } } on("get by an invalid key using `get`") { it("should throw NoSuchPathException") { assertThat( { sourceForKey[invalidKey] }, throws(has(NoSuchPathException::path, equalTo(invalidKey.toPath()))) ) } } } group("cast operation") { on("cast int to long") { val source = 1.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1L)) } } on("cast short to int") { val source = 1.toShort().asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1)) } } on("cast byte to short") { val source = 1.toByte().asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.toShort())) } } on("cast long to BigInteger") { val source = 1L.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(BigInteger.valueOf(1))) } } on("cast double to BigDecimal") { val source = 1.5.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(BigDecimal.valueOf(1.5))) } } on("cast long in range of int to int") { val source = 1L.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1)) } } on("cast long out of range of int to int") { it("should throw ParseException") { assertThat({ Long.MAX_VALUE.asSource().asValue() }, throws()) assertThat({ Long.MIN_VALUE.asSource().asValue() }, throws()) } } on("cast int in range of short to short") { val source = 1.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.toShort())) } } on("cast int out of range of short to short") { it("should throw ParseException") { assertThat({ Int.MAX_VALUE.asSource().asValue() }, throws()) assertThat({ Int.MIN_VALUE.asSource().asValue() }, throws()) } } on("cast short in range of byte to byte") { val source = 1.toShort().asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.toByte())) } } on("cast short out of range of byte to byte") { it("should throw ParseException") { assertThat({ Short.MAX_VALUE.asSource().asValue() }, throws()) assertThat({ Short.MIN_VALUE.asSource().asValue() }, throws()) } } on("cast long in range of byte to byte") { val source = 1L.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1L.toByte())) } } on("cast long out of range of byte to byte") { it("should throw ParseException") { assertThat({ Long.MAX_VALUE.asSource().asValue() }, throws()) assertThat({ Long.MIN_VALUE.asSource().asValue() }, throws()) } } on("cast double to float") { val source = 1.5.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.5f)) } } on("cast char to string") { val source = 'a'.asSource() it("should succeed") { assertThat(source.asValue(), equalTo("a")) } } on("cast string containing single char to char") { val source = "a".asSource() it("should succeed") { assertThat(source.asValue(), equalTo('a')) } } on("cast string containing multiple chars to char") { val source = "ab".asSource() it("should throw ParseException") { assertThat({ source.asValue() }, throws()) } } on("cast \"true\" to Boolean") { val source = "true".asSource() it("should succeed") { assertTrue { source.asValue() } } } on("cast \"false\" to Boolean") { val source = "false".asSource() it("should succeed") { assertFalse { source.asValue() } } } on("cast string with invalid format to Boolean") { val source = "yes".asSource() it("should throw ParseException") { assertThat({ source.asValue() }, throws()) } } on("cast string to Byte") { val source = "1".asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.toByte())) } } on("cast string to Short") { val source = "1".asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.toShort())) } } on("cast string to Int") { val source = "1".asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1)) } } on("cast string to Long") { val source = "1".asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1L)) } } on("cast string to Float") { val source = "1.5".asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.5F)) } } on("cast string to Double") { val source = "1.5F".asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.5)) } } on("cast string to BigInteger") { val source = "1".asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.toBigInteger())) } } on("cast string to BigDecimal") { val source = "1.5".asSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.5.toBigDecimal())) } } on("cast string to OffsetTime") { val text = "10:15:30+01:00" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(OffsetTime.parse(text))) } } on("cast string with invalid format to OffsetTime") { val text = "10:15:30" val source = text.asSource() it("should throw ParseException") { assertThat({ source.asValue() }, throws()) } } on("cast string to OffsetDateTime") { val text = "2007-12-03T10:15:30+01:00" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(OffsetDateTime.parse(text))) } } on("cast string to ZonedDateTime") { val text = "2007-12-03T10:15:30+01:00[Europe/Paris]" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(ZonedDateTime.parse(text))) } } on("cast string to LocalDate") { val text = "2007-12-03" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(LocalDate.parse(text))) } } on("cast string to LocalTime") { val text = "10:15:30" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(LocalTime.parse(text))) } } on("cast string to LocalDateTime") { val text = "2007-12-03T10:15:30" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(LocalDateTime.parse(text))) } } on("cast string to Year") { val text = "2007" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(Year.parse(text))) } } on("cast string to YearMonth") { val text = "2007-12" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(YearMonth.parse(text))) } } on("cast string to Instant") { val text = "2007-12-03T10:15:30.00Z" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(Instant.parse(text))) } } on("cast string to Date") { val text = "2007-12-03T10:15:30.00Z" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(Date.from(Instant.parse(text)))) } } on("cast LocalDateTime string to Date") { val text = "2007-12-03T10:15:30" val source = text.asSource() it("should succeed") { assertThat( source.asValue(), equalTo(Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC))) ) } } on("cast LocalDate string to Date") { val text = "2007-12-03" val source = text.asSource() it("should succeed") { assertThat( source.asValue(), equalTo(Date.from(LocalDate.parse(text).atStartOfDay().toInstant(ZoneOffset.UTC))) ) } } on("cast string to Duration") { val text = "P2DT3H4M" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(Duration.parse(text))) } } on("cast string with simple unit to Duration") { val text = "200ms" val source = text.asSource() it("should succeed") { assertThat(source.asValue(), equalTo(Duration.ofMillis(200))) } } on("cast string with invalid format to Duration") { val text = "2 year" val source = text.asSource() it("should throw ParseException") { assertThat({ source.asValue() }, throws()) } } on("cast string to SizeInBytes") { val text = "10k" val source = text.asSource() it("should succeed") { assertThat(source.asValue().bytes, equalTo(10240L)) } } on("cast string with invalid format to SizeInBytes") { val text = "10u" val source = text.asSource() it("should throw ParseException") { assertThat({ source.asValue() }, throws()) } } on("cast set to list") { val source = setOf(1).asSource() it("should succeed") { assertThat(source.asValue>(), equalTo(listOf(1))) } } on("cast array to list") { val source = arrayOf(1).asSource() it("should succeed") { assertThat(source.asValue>(), equalTo(listOf(1))) } } on("cast array to set") { val source = arrayOf(1).asSource() it("should succeed") { assertThat(source.asValue>(), equalTo(setOf(1))) } } } group("load operation") { on("load from valid source") { it("should load successfully") { val config = load(1) assertThat(config("item"), equalTo(1)) } } on("load concrete map type") { it("should load successfully") { val config = load>(mapOf("1" to 1)) assertThat(config>("item"), equalTo(mapOf("1" to 1))) } } on("load invalid enum value") { it("should throw LoadException caused by ParseException") { assertCausedBy { load("NO_HEAP") } } } on("load unsupported simple type value") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { load(mapOf("invalid" to "anon")) } } } on("load map with unsupported key type") { it("should throw LoadException caused by UnsupportedMapKeyException") { assertCausedBy { load, String>>(mapOf((1 to 1) to "1")) } } } on("load invalid enum value") { it("should throw LoadException caused by ParseException") { assertCausedBy { load("NO_HEAP") } } } on("load invalid POJO value") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { load(mapOf("name" to Source())) } } } on("load when SUBSTITUTE_SOURCE_WHEN_LOADED is disabled on config") { val source = mapOf("item" to mapOf("key1" to "a", "key2" to "b\${item.key1}")).asSource() val config = Config { addSpec( object : ConfigSpec() { @Suppress("unused") val item by required>() } ) }.disable(Feature.SUBSTITUTE_SOURCE_BEFORE_LOADED) .withSource(source) it("should not substitute path variables before loaded") { assertThat( config>("item"), equalTo(mapOf("key1" to "a", "key2" to "b\${item.key1}")) ) } } on("load when SUBSTITUTE_SOURCE_WHEN_LOADED is disabled on source") { val source = mapOf("item" to mapOf("key1" to "a", "key2" to "b\${item.key1}")).asSource() .disabled(Feature.SUBSTITUTE_SOURCE_BEFORE_LOADED) val config = Config { addSpec( object : ConfigSpec() { @Suppress("unused") val item by required>() } ) }.withSource(source) it("should substitute path variables before loaded") { assertThat( config>("item"), equalTo(mapOf("key1" to "a", "key2" to "b\${item.key1}")) ) } } on("load when SUBSTITUTE_SOURCE_WHEN_LOADED is enabled") { val source = mapOf("item" to mapOf("key1" to "a", "key2" to "b\${item.key1}")).asSource() val config = Config { addSpec( object : ConfigSpec() { @Suppress("unused") val item by required>() } ) }.withSource(source) it("should substitute path variables") { assertTrue { Feature.SUBSTITUTE_SOURCE_BEFORE_LOADED.enabledByDefault } assertThat( config>("item"), equalTo(mapOf("key1" to "a", "key2" to "ba")) ) } } } group("substitution operation") { on("doesn't contain any path variable") { val map = mapOf("key1" to "a", "key2" to "b") val source = map.asSource().substituted() it("should keep it unchanged") { assertThat(source.tree.toHierarchical(), equalTo(map)) } } on("contains single path variable") { val map = mapOf("key1" to "a", "key2" to "b\${key1}") val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "ba")) ) } } on("contains integer path variable") { val map = mapOf("key1" to 1, "key2" to "b\${key1}", "key3" to "\${key1}") val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to 1, "key2" to "b1", "key3" to 1)) ) } } on("contains path variables with string list value") { val map = mapOf("key1" to "a,b,c", "key2" to "a\${key1}") val source = Source.from.map.flat(map).substituted().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo( mapOf( "key1" to "a,b,c", "key2" to "aa,b,c" ) ) ) } } on("contains path variables in list") { val map = mapOf("top" to listOf(mapOf("key1" to "a", "key2" to "b\${top.0.key1}"))) val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("top" to listOf(mapOf("key1" to "a", "key2" to "ba")))) ) } } on("contains path variable with wrong type") { val map = mapOf("key1" to 1.0, "key2" to "b\${key1}") it("should throw WrongTypeException") { assertThrows { map.asSource().substituted() } } } on("contains escaped path variables") { val map = mapOf("key1" to "a", "key2" to "b\$\${key1}") val source = map.asSource().substituted() it("should not substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "b\${key1}")) ) } } on("contains nested escaped path variables") { val map = mapOf("key1" to "a", "key2" to "b\$\$\$\${key1}") val source = map.asSource().substituted() it("should escaped only once") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "b\$\$\${key1}")) ) } } on("contains nested escaped path variables and substitute multiple times") { val map = mapOf("key1" to "a", "key2" to "b\$\$\$\${key1}") val source = map.asSource().substituted().substituted() it("should escaped only once") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "b\$\$\${key1}")) ) } } on("contains undefined path variable") { val map = mapOf("key2" to "b\${key1}") it("should throw UndefinedPathVariableException by default") { assertThat( { map.asSource().substituted() }, throws(has(UndefinedPathVariableException::text, equalTo("b\${key1}"))) ) } it("should keep unsubstituted when errorWhenUndefined is `false`") { val source = map.asSource().substituted(errorWhenUndefined = false) assertThat(source.tree.toHierarchical(), equalTo(map)) } } on("contains undefined path variable in reference format") { val map = mapOf("key2" to "\${key1}") it("should throw UndefinedPathVariableException by default") { assertThat( { map.asSource().substituted() }, throws(has(UndefinedPathVariableException::text, equalTo("\${key1}"))) ) } it("should keep unsubstituted when errorWhenUndefined is `false`") { val source = map.asSource().substituted(errorWhenUndefined = false) assertThat(source.tree.toHierarchical(), equalTo(map)) } } on("contains multiple path variables") { val map = mapOf("key1" to "a", "key2" to "\${key1}b\${key3}", "key3" to "c") val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "abc", "key3" to "c")) ) } } on("contains chained path variables") { val map = mapOf("key1" to "a", "key2" to "\${key1}b", "key3" to "\${key2}c") val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "ab", "key3" to "abc")) ) } } on("contains nested path variables") { val map = mapOf("key1" to "a", "key2" to "\${\${key3}}b", "key3" to "key1") val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "ab", "key3" to "key1")) ) } } on("contains a path variable with default value") { val map = mapOf("key1" to "a", "key2" to "b\${key3:-c}") val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "bc")) ) } } on("contains a path variable with key") { val map = mapOf("key1" to "a", "key2" to "\${key1}\${base64Decoder:SGVsbG9Xb3JsZCE=}") val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "aHelloWorld!")) ) } } on("contains a path variable in reference format") { val map = mapOf("key1" to mapOf("key3" to "a", "key4" to "b"), "key2" to "\${key1}") val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo( mapOf( "key1" to mapOf("key3" to "a", "key4" to "b"), "key2" to mapOf("key3" to "a", "key4" to "b") ) ) ) } } on("contains nested path variable in reference format") { val map = mapOf("key1" to mapOf("key3" to "a", "key4" to "b"), "key2" to "\${\${key3}}", "key3" to "key1") val source = map.asSource().substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo( mapOf( "key1" to mapOf("key3" to "a", "key4" to "b"), "key2" to mapOf("key3" to "a", "key4" to "b"), "key3" to "key1" ) ) ) } } on("contains path variable in different sources") { val map1 = mapOf("key1" to "a") val map2 = mapOf("key2" to "b\${key1}") val source = (map2.asSource() + map1.asSource()).substituted() it("should substitute path variables") { assertThat( source.tree.toHierarchical(), equalTo(mapOf("key1" to "a", "key2" to "ba")) ) } } } group("feature operation") { on("enable feature") { val source = Source().enabled(Feature.FAIL_ON_UNKNOWN_PATH) it("should let the feature be enabled") { assertTrue { source.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH) } } } on("disable feature") { val source = Source().disabled(Feature.FAIL_ON_UNKNOWN_PATH) it("should let the feature be disabled") { assertFalse { source.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH) } } } on("enable feature before transforming source") { val source = Source().enabled(Feature.FAIL_ON_UNKNOWN_PATH).withPrefix("prefix") it("should let the feature be enabled") { assertTrue { source.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH) } } } on("disable feature before transforming source") { val source = Source().disabled(Feature.FAIL_ON_UNKNOWN_PATH).withPrefix("prefix") it("should let the feature be disabled") { assertFalse { source.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH) } } } on("by default") { val source = Source() it("should use the feature's default setting") { assertThat( source.isEnabled(Feature.FAIL_ON_UNKNOWN_PATH), equalTo(Feature.FAIL_ON_UNKNOWN_PATH.enabledByDefault) ) } } } group("source with prefix") { val source by memoized { Prefix("level1.level2") + mapOf("key" to "value").asSource() } on("prefix is empty") { it("should return itself") { assertThat(source.withPrefix(""), sameInstance(source)) } } on("find a valid path") { it("should contain the value") { assertTrue("level1" in source) assertTrue("level1.level2" in source) assertTrue("level1.level2.key" in source) } } on("find an invalid path") { it("should not contain the value") { assertTrue("level3" !in source) assertTrue("level1.level3" !in source) assertTrue("level1.level2.level3" !in source) assertTrue("level1.level3.level2" !in source) } } on("get by a valid path using `getOrNull`") { it("should return the corresponding value") { assertThat( (source.getOrNull("level1")?.get("level2.key")?.tree as ValueNode).value as String, equalTo("value") ) assertThat( (source.getOrNull("level1.level2")?.get("key")?.tree as ValueNode).value as String, equalTo("value") ) assertThat( (source.getOrNull("level1.level2.key")?.tree as ValueNode).value as String, equalTo("value") ) } } on("get by an invalid path using `getOrNull`") { it("should return null") { assertThat(source.getOrNull("level3"), absent()) assertThat(source.getOrNull("level1.level3"), absent()) assertThat(source.getOrNull("level1.level2.level3"), absent()) assertThat(source.getOrNull("level1.level3.level2"), absent()) } } } } }) private inline fun load(value: Any): Config = Config().apply { addSpec( object : ConfigSpec() { @Suppress("unused") val item by required() } ) }.withSource(mapOf("item" to value).asSource()) private data class Person(val name: String) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/WriterSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.properties.toProperties import com.uchuhimo.konf.tempFile import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.io.ByteArrayOutputStream import java.io.StringWriter import java.nio.charset.Charset import kotlin.test.assertTrue object WriterSpec : SubjectSpek({ subject { val config = Config { addSpec( object : ConfigSpec() { val key by optional("value") } ) } config.toProperties } given("a writer") { val expectedString = "key=value" + System.lineSeparator() on("save to string") { val string = subject.toText() it("should return a string which contains content from config") { assertThat(string, equalTo(expectedString)) } } on("save to byte array") { val byteArray = subject.toBytes() it("should return a byte array which contains content from config") { assertThat(byteArray.toString(Charset.defaultCharset()), equalTo(expectedString)) } } on("save to writer") { val writer = StringWriter() subject.toWriter(writer) it("should return a writer which contains content from config") { assertThat(writer.toString(), equalTo(expectedString)) } } on("save to output stream") { val outputStream = ByteArrayOutputStream() subject.toOutputStream(outputStream) it("should return an output stream which contains content from config") { assertThat(outputStream.toString(), equalTo(expectedString)) } } on("save to file") { val file = tempFile() subject.toFile(file) it("should return a file which contains content from config") { assertThat(file.readText(), equalTo(expectedString)) } it("should not lock the file") { assertTrue { file.delete() } } } on("save to file by path") { val file = tempFile() val path = file.toString() subject.toFile(path) it("should return a file which contains content from config") { assertThat(file.readText(), equalTo(expectedString)) } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/base/FlatSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.ConfigForLoad import com.uchuhimo.konf.source.Source import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object FlatSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) enable(Feature.FAIL_ON_UNKNOWN_PATH) }.from.map.flat(loadContent) } itBehavesLike(FlatSourceLoadBaseSpec) }) object FlatSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) }.from.map.flat(loadContent) Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) }.from.map.flat(config.toFlatMap()) } itBehavesLike(FlatSourceLoadBaseSpec) }) object FlatSourceFromDefaultProvidersSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) }.withSource(Source.from.map.flat(loadContent)) } itBehavesLike(FlatSourceLoadBaseSpec) }) private val loadContent = mapOf( "empty" to "null", "literalEmpty" to "null", "present" to "1", "boolean" to "false", "int" to "1", "short" to "2", "byte" to "3", "bigInteger" to "4", "long" to "4", "double" to "1.5", "float" to "-1.5", "bigDecimal" to "1.5", "char" to "a", "string" to "string", "offsetTime" to "10:15:30+01:00", "offsetDateTime" to "2007-12-03T10:15:30+01:00", "zonedDateTime" to "2007-12-03T10:15:30+01:00[Europe/Paris]", "localDate" to "2007-12-03", "localTime" to "10:15:30", "localDateTime" to "2007-12-03T10:15:30", "date" to "2007-12-03T10:15:30Z", "year" to "2007", "yearMonth" to "2007-12", "instant" to "2007-12-03T10:15:30.00Z", "duration" to "P2DT3H4M", "simpleDuration" to "200millis", "size" to "10k", "enum" to "LABEL2", "list" to "1,2,3", "mutableList" to "1,2,3", "listOfList.0" to "1,2", "listOfList.1" to "3,4", "set" to "1,2,1", "sortedSet" to "2,1,1,3", "map.a" to "1", "map.b" to "2", "map.c" to "3", "intMap.1" to "a", "intMap.2" to "b", "intMap.3" to "c", "sortedMap.c" to "3", "sortedMap.b" to "2", "sortedMap.a" to "1", "nested.0.0.0.a" to "1", "listOfMap.0.a" to "1", "listOfMap.0.b" to "2", "listOfMap.1.a" to "3", "listOfMap.1.b" to "4", "array.boolean" to "true,false", "array.byte" to "1,2,3", "array.short" to "1,2,3", "array.int" to "1,2,3", "array.long" to "4,5,6", "array.float" to "-1, 0.0, 1", "array.double" to "-1, 0.0, 1", "array.char" to "a,b,c", "array.object.boolean" to "true,false", "array.object.int" to "1,2,3", "array.object.string" to "one,two,three", "array.object.enum" to "LABEL1,LABEL2,LABEL3", "pair.first" to "1", "pair.second" to "2", "clazz.empty" to "null", "clazz.literalEmpty" to "null", "clazz.present" to "1", "clazz.boolean" to "false", "clazz.int" to "1", "clazz.short" to "2", "clazz.byte" to "3", "clazz.bigInteger" to "4", "clazz.long" to "4", "clazz.double" to "1.5", "clazz.float" to "-1.5", "clazz.bigDecimal" to "1.5", "clazz.char" to "a", "clazz.string" to "string", "clazz.offsetTime" to "10:15:30+01:00", "clazz.offsetDateTime" to "2007-12-03T10:15:30+01:00", "clazz.zonedDateTime" to "2007-12-03T10:15:30+01:00[Europe/Paris]", "clazz.localDate" to "2007-12-03", "clazz.localTime" to "10:15:30", "clazz.localDateTime" to "2007-12-03T10:15:30", "clazz.date" to "2007-12-03T10:15:30Z", "clazz.year" to "2007", "clazz.yearMonth" to "2007-12", "clazz.instant" to "2007-12-03T10:15:30.00Z", "clazz.duration" to "P2DT3H4M", "clazz.simpleDuration" to "200millis", "clazz.size" to "10k", "clazz.enum" to "LABEL2", "clazz.booleanArray" to "true,false", "clazz.nested.0.0.0.a" to "1", "emptyList" to "", "emptySet" to "", "emptyArray" to "", "emptyObjectArray" to "", "singleElementList" to "1", "multipleElementsList" to "1,2", "flatClass.stringWithComma" to "string,with,comma", "flatClass.emptyList" to "", "flatClass.emptySet" to "", "flatClass.emptyArray" to "", "flatClass.emptyObjectArray" to "", "flatClass.singleElementList" to "1", "flatClass.multipleElementsList" to "1,2" ).mapKeys { (key, _) -> "level1.level2.$key" } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/base/FlatSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.throws import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.InvalidPathException import com.uchuhimo.konf.ListNode import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.source.ParseException import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.toPath import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.junit.jupiter.api.assertThrows import kotlin.test.assertTrue object FlatSourceSpec : SubjectSpek({ given("a flat map source") { group("get operation") { val source by memoized { FlatSource(map = mapOf("level1.level2.key" to "value")) } on("get the underlying map") { it("should return the specified map") { assertThat(source.map, equalTo(mapOf("level1.level2.key" to "value"))) } } on("access with empty path") { it("should contain the path") { assertTrue("".toPath() in source) } it("should return itself in `getOrNull`") { assertThat(source.getOrNull("".toPath()), equalTo(source as Source)) } } } group("get operation for list value") { val source by memoized { FlatSource( map = mapOf( "empty" to "", "single" to "a", "multiple" to "a,b" ) ) } on("empty string value") { it("should return an empty list") { assertThat((source["empty"].tree as ListNode).list, equalTo(listOf())) } } on("string value without commas") { it("should return a list containing a single element") { assertThat( (source["single"].tree as ListNode).list.map { (it as ValueNode).value as String }, equalTo(listOf("a")) ) } } on("string value with commas") { it("should return a list containing multiple elements") { assertThat( (source["multiple"].tree as ListNode).list.map { (it as ValueNode).value as String }, equalTo(listOf("a", "b")) ) } } } on("contain invalid key") { it("should throw InvalidPathException") { assertThrows { FlatSource(map = mapOf("level1.level2.key." to "value")) } } } group("cast operation") { val source by memoized { FlatSource(map = mapOf("level1.key" to "value"))["level1.key"] } on("value is a string") { it("should succeed to cast to string") { assertThat(source.asValue(), equalTo("value")) } } on("value is not a boolean") { it("should throw ParseException when casting to boolean") { assertThat({ source.asValue() }, throws()) } } on("value is not a double") { it("should throw ParseException when casting to double") { assertThat({ source.asValue() }, throws()) } } on("value is not an integer") { it("should throw ParseException when casting to integer") { assertThat({ source.asValue() }, throws()) } } on("value is not a long") { it("should throw ParseException when casting to long") { assertThat({ source.asValue() }, throws()) } } } } given("a config that contains list of strings with commas") { val spec = object : ConfigSpec() { @Suppress("unused") val list by optional(listOf("a,b", "c, d")) } val config = Config { addSpec(spec) } val map = config.toFlatMap() it("should not be joined into a string") { assertThat(map["list.0"], equalTo("a,b")) assertThat(map["list.1"], equalTo("c, d")) } } given("a config that contains list of strings without commas") { val spec = object : ConfigSpec() { @Suppress("unused") val list by optional(listOf("a", "b", "c", "d")) } val config = Config { addSpec(spec) } val map = config.toFlatMap() it("should be joined into a string with commas") { assertThat(map["list"], equalTo("a,b,c,d")) } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/base/KVSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.toPath import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue object KVSourceSpec : SubjectSpek({ subject { KVSource(map = mapOf("1" to 1)) } given("a KV source") { on("get the underlying map") { it("should return the specified map") { assertThat(subject.map, equalTo(mapOf("1" to 1 as Any))) } } on("get an existed key") { it("should contain the key") { assertTrue("1".toPath() in subject) } it("should contain the corresponding value") { assertThat(subject.getOrNull("1".toPath())?.asValue(), equalTo(1)) } } on("get an non-existed key") { it("should not contain the key") { assertFalse("2".toPath() in subject) } it("should not contain the corresponding value") { assertNull(subject.getOrNull("2".toPath())) } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/base/MapSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.ConfigForLoad import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.SourceLoadBaseSpec import com.uchuhimo.konf.source.toDuration import com.uchuhimo.konf.toSizeInBytes import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike import java.math.BigDecimal import java.math.BigInteger import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.OffsetDateTime import java.time.OffsetTime import java.time.Year import java.time.YearMonth import java.time.ZonedDateTime import java.util.Date object MapSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) enable(Feature.FAIL_ON_UNKNOWN_PATH) }.from.map.hierarchical(loadContent) } itBehavesLike(SourceLoadBaseSpec) }) object MapSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) }.from.map.hierarchical(loadContent) Config { addSpec(ConfigForLoad) }.from.map.hierarchical(config.toHierarchicalMap()) } itBehavesLike(SourceLoadBaseSpec) }) object MapSourceFromDefaultProvidersSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) enable(Feature.FAIL_ON_UNKNOWN_PATH) }.withSource(Source.from.map.hierarchical(loadContent)) } itBehavesLike(SourceLoadBaseSpec) }) private val loadContent = mapOf( "level1" to mapOf( "level2" to mapOf( "empty" to "null", "literalEmpty" to "null", "present" to 1, "boolean" to false, "int" to 1, "short" to 2.toShort(), "byte" to 3.toByte(), "bigInteger" to BigInteger.valueOf(4), "long" to 4L, "double" to 1.5, "float" to -1.5f, "bigDecimal" to BigDecimal.valueOf(1.5), "char" to 'a', "string" to "string", "offsetTime" to OffsetTime.parse("10:15:30+01:00"), "offsetDateTime" to OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), "zonedDateTime" to ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), "localDate" to LocalDate.parse("2007-12-03"), "localTime" to LocalTime.parse("10:15:30"), "localDateTime" to LocalDateTime.parse("2007-12-03T10:15:30"), "date" to Date.from(Instant.parse("2007-12-03T10:15:30Z")), "year" to Year.parse("2007"), "yearMonth" to YearMonth.parse("2007-12"), "instant" to Instant.parse("2007-12-03T10:15:30.00Z"), "duration" to "P2DT3H4M".toDuration(), "simpleDuration" to "200millis".toDuration(), "size" to "10k".toSizeInBytes(), "enum" to "LABEL2", "array" to mapOf( "boolean" to listOf(true, false), "byte" to listOf(1, 2, 3), "short" to listOf(1, 2, 3), "int" to listOf(1, 2, 3), "long" to listOf(4L, 5L, 6L), "float" to listOf(-1.0F, 0.0F, 1.0F), "double" to listOf(-1.0, 0.0, 1.0), "char" to listOf('a', 'b', 'c'), "object" to mapOf( "boolean" to listOf(true, false), "int" to listOf(1, 2, 3), "string" to listOf("one", "two", "three"), "enum" to listOf("LABEL1", "LABEL2", "LABEL3") ) ), "list" to listOf(1, 2, 3), "mutableList" to listOf(1, 2, 3), "listOfList" to listOf(listOf(1, 2), listOf(3, 4)), "set" to listOf(1, 2, 1), "sortedSet" to listOf(2, 1, 1, 3), "map" to mapOf( "a" to 1, "b" to 2, "c" to 3 ), "intMap" to mapOf( 1 to "a", 2 to "b", 3 to "c" ), "sortedMap" to mapOf( "c" to 3, "b" to 2, "a" to 1 ), "listOfMap" to listOf( mapOf("a" to 1, "b" to 2), mapOf("a" to 3, "b" to 4) ), "nested" to listOf(listOf(listOf(mapOf("a" to 1)))), "pair" to mapOf("first" to 1, "second" to 2), "clazz" to mapOf( "empty" to "null", "literalEmpty" to "null", "present" to 1, "boolean" to false, "int" to 1, "short" to 2.toShort(), "byte" to 3.toByte(), "bigInteger" to BigInteger.valueOf(4), "long" to 4L, "double" to 1.5, "float" to -1.5f, "bigDecimal" to BigDecimal.valueOf(1.5), "char" to 'a', "string" to "string", "offsetTime" to OffsetTime.parse("10:15:30+01:00"), "offsetDateTime" to OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), "zonedDateTime" to ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), "localDate" to LocalDate.parse("2007-12-03"), "localTime" to LocalTime.parse("10:15:30"), "localDateTime" to LocalDateTime.parse("2007-12-03T10:15:30"), "date" to Date.from(Instant.parse("2007-12-03T10:15:30Z")), "year" to Year.parse("2007"), "yearMonth" to YearMonth.parse("2007-12"), "instant" to Instant.parse("2007-12-03T10:15:30.00Z"), "duration" to "P2DT3H4M".toDuration(), "simpleDuration" to "200millis".toDuration(), "size" to "10k".toSizeInBytes(), "enum" to "LABEL2", "booleanArray" to listOf(true, false), "nested" to listOf(listOf(listOf(mapOf("a" to 1)))) ) ) ) ) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/base/MapSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.toPath import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue object MapSourceSpec : SubjectSpek({ subject { MapSource(map = mapOf("1" to 1)) } given("a map source") { on("get the underlying map") { it("should return the specified map") { assertThat(subject.map, equalTo(mapOf("1" to 1 as Any))) } } on("cast to map") { it("should succeed") { val map = subject.tree.children assertThat((map["1"] as ValueNode).value, equalTo(1 as Any)) } } on("get an existed key") { it("should contain the key") { assertTrue("1".toPath() in subject) } it("should contain the corresponding value") { assertThat(subject.getOrNull("1".toPath())?.asValue(), equalTo(1)) } } on("get an non-existed key") { it("should not contain the key") { assertFalse("2".toPath() in subject) } it("should not contain the corresponding value") { assertNull(subject.getOrNull("2".toPath())) } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/base/ValueSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.sameInstance import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.NoSuchPathException import com.uchuhimo.konf.source.asSource import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on object ValueSourceSpec : Spek({ given("a value source") { on("get with non-empty path") { it("should throw NoSuchPathException") { assertThat({ 1.asSource()["a"] }, throws()) } } on("invoke `asSource`") { val source = 1.asSource() it("should return itself") { assertThat(source.asSource(), sameInstance(source)) } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/deserializer/DurationDeserializerSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.ObjectMappingException import com.uchuhimo.konf.source.assertCausedBy import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import java.time.Duration object DurationDeserializerSpec : Spek({ val spec = object : ConfigSpec() { val item by required() } val config by memoized { Config { addSpec(spec) } } given("a duration deserializer") { on("deserialize valid string") { config.from.map.kv(mapOf("item" to mapOf("duration" to "P2DT3H4M"))).apply { it("should succeed") { assertThat( this@apply[spec.item].duration, equalTo(Duration.parse("P2DT3H4M")) ) } } } on("deserialize empty string") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.from.map.kv(mapOf("item" to mapOf("duration" to " "))) } } } on("deserialize value with invalid type") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.from.map.kv(mapOf("item" to mapOf("duration" to 1))) } } } on("deserialize value with invalid format") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.from.map.kv(mapOf("item" to mapOf("duration" to "*1s"))) } } } } }) private data class DurationWrapper(val duration: Duration) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/deserializer/OffsetDateTimeDeserializerSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.ObjectMappingException import com.uchuhimo.konf.source.assertCausedBy import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import java.time.OffsetDateTime object OffsetDateTimeDeserializerSpec : Spek({ val spec = object : ConfigSpec() { val item by required() } val config by memoized { Config { addSpec(spec) } } given("an OffsetDateTime deserializer") { on("deserialize valid string") { config.from.map.kv(mapOf("item" to mapOf("offsetDateTime" to "2007-12-03T10:15:30+01:00"))).apply { it("should succeed") { assertThat( this@apply[spec.item].offsetDateTime, equalTo(OffsetDateTime.parse("2007-12-03T10:15:30+01:00")) ) } } } on("deserialize empty string") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.from.map.kv(mapOf("item" to mapOf("offsetDateTime" to " "))) } } } on("deserialize value with invalid type") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.from.map.kv(mapOf("item" to mapOf("offsetDateTime" to 1))) } } } on("deserialize value with invalid format") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.from.map.kv(mapOf("item" to mapOf("offsetDateTime" to "2007-12-03T10:15:30"))) } } } } }) private data class OffsetDateTimeWrapper(val offsetDateTime: OffsetDateTime) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/deserializer/StringDeserializerSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import com.fasterxml.jackson.databind.DeserializationFeature import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.ObjectMappingException import com.uchuhimo.konf.source.assertCausedBy import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on object StringDeserializerSpec : Spek({ val spec = object : ConfigSpec() { val item by required() } val config by memoized { Config { addSpec(spec) } } given("a string deserializer") { on("deserialize string containing commas") { config.from.map.kv(mapOf("item" to mapOf("string" to "a,b,c"))).apply { it("should succeed") { assertThat(this@apply[spec.item].string, equalTo("a,b,c")) } } } on("deserialize string containing commas when UNWRAP_SINGLE_VALUE_ARRAYS is enable") { config.apply { mapper.enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) }.from.map.kv(mapOf("item" to mapOf("string" to "a,b,c"))).apply { it("should succeed") { assertThat(this@apply[spec.item].string, equalTo("a,b,c")) } } } on("deserialize string from number") { config.from.map.kv(mapOf("item" to mapOf("string" to 1))).apply { it("should succeed") { assertThat(this@apply[spec.item].string, equalTo("1")) } } } on("deserialize string from list of numbers") { config.from.map.kv(mapOf("item" to mapOf("string" to listOf(1, 2)))).apply { it("should succeed") { assertThat(this@apply[spec.item].string, equalTo("1,2")) } } } on("deserialize string from single value array") { config.apply { mapper.enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) }.from.map.kv(mapOf("item" to mapOf("string" to listOf("a")))).apply { it("should succeed") { assertThat(this@apply[spec.item].string, equalTo("a")) } } } on("deserialize string from empty array") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.apply { mapper.enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) }.from.map.kv(mapOf("item" to mapOf("string" to listOf()))) } } } } }) private data class StringWrapper(val string: String) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/deserializer/ZonedDateTimeDeserializerSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.deserializer import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.ObjectMappingException import com.uchuhimo.konf.source.assertCausedBy import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import java.time.ZonedDateTime object ZonedDateTimeDeserializerSpec : Spek({ val spec = object : ConfigSpec() { val item by required() } val config by memoized { Config { addSpec(spec) } } given("an ZonedDateTime deserializer") { on("deserialize valid string") { config.from.map.kv(mapOf("item" to mapOf("zonedDateTime" to "2007-12-03T10:15:30+01:00[Europe/Paris]"))).apply { it("should succeed") { assertThat( this@apply[spec.item].zonedDateTime, equalTo(ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]")) ) } } } on("deserialize empty string") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.from.map.kv(mapOf("item" to mapOf("zonedDateTime" to " "))) } } } on("deserialize value with invalid type") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.from.map.kv(mapOf("item" to mapOf("zonedDateTime" to 1))) } } } on("deserialize value with invalid format") { it("should throw LoadException caused by ObjectMappingException") { assertCausedBy { config.from.map.kv(mapOf("item" to mapOf("zonedDateTime" to "2007-12-03T10:15:30"))) } } } } }) private data class ZonedDateTimeWrapper(val zonedDateTime: ZonedDateTime) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/env/EnvProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.env import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike import kotlin.test.assertTrue object EnvProviderSpec : SubjectSpek({ subject { EnvProvider } given("a source provider") { on("create source from system environment") { val source = subject.env() it("should have correct type") { assertThat(source.info["type"], equalTo("system-environment")) } it("should return a source which contains value from system environment") { val config = Config { addSpec(SourceSpec) }.withSource(source) assertThat(config[SourceSpec.Test.type], equalTo("env")) assertTrue { config[SourceSpec.camelCase] } } it("should return a case-insensitive source") { val config = Config().withSource(source).apply { addSpec(SourceSpec) } assertThat(config[SourceSpec.Test.type], equalTo("env")) assertTrue { config[SourceSpec.camelCase] } } } on("create flatten source from system environment") { val source = subject.env(nested = false) it("should return a source which contains value from system environment") { val config = Config { addSpec(FlattenSourceSpec) }.withSource(source) assertThat(config[FlattenSourceSpec.SOURCE_TEST_TYPE], equalTo("env")) assertTrue { config[FlattenSourceSpec.SOURCE_CAMELCASE] } } } } }) object EnvProviderInJavaSpec : SubjectSpek({ subject { EnvProvider.get() } itBehavesLike(EnvProviderSpec) }) object SourceSpec : ConfigSpec() { object Test : ConfigSpec() { val type by required() } val camelCase by required() } object FlattenSourceSpec : ConfigSpec("") { val SOURCE_CAMELCASE by required() val SOURCE_TEST_TYPE by required() } ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/env/env.properties ================================================ # # Copyright 2017-2019 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # SOURCE_TEST_TYPE=env SOURCE_CAMELCASE=true ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/json/JsonProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.json import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object JsonProviderSpec : SubjectSpek({ subject { JsonProvider } given("a JSON provider") { on("create source from reader") { //language=Json val source = subject.reader("""{ "type": "reader" }""".reader()) it("should have correct type") { assertThat(source.info["type"], equalTo("JSON")) } it("should return a source which contains value from reader") { assertThat(source["type"].asValue(), equalTo("reader")) } } on("create source from input stream") { //language=Json val source = subject.inputStream(tempFileOf("""{ "type": "inputStream" }""").inputStream()) it("should have correct type") { assertThat(source.info["type"], equalTo("JSON")) } it("should return a source which contains value from input stream") { assertThat(source["type"].asValue(), equalTo("inputStream")) } } on("create source from an empty file") { val file = tempFileOf("") it("should return an empty source") { assertThat( subject.file(file).tree.children, equalTo(mutableMapOf()) ) } } } }) object JsonProviderInJavaSpec : SubjectSpek({ subject { JsonProvider.get() } itBehavesLike(JsonProviderSpec) }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/json/JsonSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.json import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.ConfigForLoad import com.uchuhimo.konf.source.SourceLoadBaseSpec import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object JsonSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) enable(Feature.FAIL_ON_UNKNOWN_PATH) }.from.json.resource("source/source.json") } itBehavesLike(SourceLoadBaseSpec) }) object JsonSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) }.from.json.resource("source/source.json") val json = config.toJson.toText() Config { addSpec(ConfigForLoad) }.from.json.string(json) } itBehavesLike(SourceLoadBaseSpec) }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/json/JsonSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.json import com.fasterxml.jackson.databind.node.BigIntegerNode import com.fasterxml.jackson.databind.node.BooleanNode import com.fasterxml.jackson.databind.node.DecimalNode import com.fasterxml.jackson.databind.node.DoubleNode import com.fasterxml.jackson.databind.node.FloatNode import com.fasterxml.jackson.databind.node.IntNode import com.fasterxml.jackson.databind.node.LongNode import com.fasterxml.jackson.databind.node.ShortNode import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.WrongTypeException import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.toPath import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import java.math.BigDecimal import java.math.BigInteger import kotlin.test.assertNull import kotlin.test.assertTrue object JsonSourceSpec : Spek({ given("a JSON source") { group("get operation") { //language=Json val source by memoized { JsonProvider.string("""{ "key": 1 }""") } on("get underlying JSON node") { val intSource = JsonSource(IntNode.valueOf(1)) it("should return corresponding node") { val node = intSource.node assertTrue(node.isInt) assertThat(node.intValue(), equalTo(1)) } } on("get an existed key") { it("should contain the key") { assertTrue("key".toPath() in source) } it("should contain the corresponding value") { assertThat(source["key".toPath()].asValue(), equalTo(1)) } } on("get an non-existed key") { it("should not contain the key") { assertTrue("invalid".toPath() !in source) } it("should not contain the corresponding value") { assertNull(source.getOrNull("invalid".toPath())) } } } group("cast operation") { on("get string from other source") { it("should throw WrongTypeException") { assertThat({ JsonSource(IntNode.valueOf(1)).asValue() }, throws()) } } on("get boolean from other source") { it("should throw WrongTypeException") { assertThat({ JsonSource(IntNode.valueOf(1)).asValue() }, throws()) } } on("get double from other source") { it("should throw WrongTypeException") { assertThat({ JsonSource(BooleanNode.valueOf(true)).asValue() }, throws()) } } on("get integer from other source") { it("should throw WrongTypeException") { assertThat({ JsonSource(DoubleNode.valueOf(1.0)).asValue() }, throws()) } } on("get long from long source") { it("should succeed") { assertThat(JsonSource(LongNode.valueOf(1L)).asValue(), equalTo(1L)) } } on("get long from integer source") { it("should succeed") { assertThat(JsonSource(IntNode.valueOf(1)).asValue(), equalTo(1L)) } } on("get short from short source") { it("should succeed") { assertThat(JsonSource(ShortNode.valueOf(1)).asValue(), equalTo(1.toShort())) } } on("get short from integer source") { it("should succeed") { assertThat(JsonSource(IntNode.valueOf(1)).asValue(), equalTo(1.toShort())) } } on("get float from float source") { it("should succeed") { assertThat(JsonSource(FloatNode.valueOf(1.0F)).asValue(), equalTo(1.0F)) } } on("get float from double source") { it("should succeed") { assertThat(JsonSource(DoubleNode.valueOf(1.0)).asValue(), equalTo(1.0F)) } } on("get BigInteger from BigInteger source") { it("should succeed") { assertThat( JsonSource(BigIntegerNode.valueOf(BigInteger.valueOf(1L))).asValue(), equalTo(BigInteger.valueOf(1L)) ) } } on("get BigInteger from long source") { it("should succeed") { assertThat( JsonSource(LongNode.valueOf(1L)).asValue(), equalTo(BigInteger.valueOf(1L)) ) } } on("get BigDecimal from BigDecimal source") { it("should succeed") { assertThat( JsonSource(DecimalNode.valueOf(BigDecimal.valueOf(1.0))).asValue(), equalTo(BigDecimal.valueOf(1.0)) ) } } on("get BigDecimal from double source") { it("should succeed") { assertThat( JsonSource(DoubleNode.valueOf(1.0)).asValue(), equalTo(BigDecimal.valueOf(1.0)) ) } } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/json/JsonWriterSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.json import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.Writer import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.io.ByteArrayOutputStream import java.io.StringWriter object JsonWriterSpec : SubjectSpek({ subject { val config = Config { addSpec( object : ConfigSpec() { val key by optional("value") } ) } config.toJson } given("a writer") { //language=Json val expectedString = """ { "key" : "value" } """.trimIndent().replace("\n", System.lineSeparator()) on("save to writer") { val writer = StringWriter() subject.toWriter(writer) it("should return a writer which contains content from config") { assertThat(writer.toString(), equalTo(expectedString)) } } on("save to output stream") { val outputStream = ByteArrayOutputStream() subject.toOutputStream(outputStream) it("should return an output stream which contains content from config") { assertThat(outputStream.toString(), equalTo(expectedString)) } } } }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/properties/PropertiesProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.properties import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object PropertiesProviderSpec : SubjectSpek({ subject { PropertiesProvider } given("a properties provider") { on("create source from reader") { val source = subject.reader("type = reader".reader()) it("should have correct type") { assertThat(source.info["type"], equalTo("properties")) } it("should return a source which contains value from reader") { assertThat(source["type"].asValue(), equalTo("reader")) } } on("create source from input stream") { val source = subject.inputStream( tempFileOf("type = inputStream").inputStream() ) it("should have correct type") { assertThat(source.info["type"], equalTo("properties")) } it("should return a source which contains value from input stream") { assertThat(source["type"].asValue(), equalTo("inputStream")) } } on("create source from system properties") { System.setProperty("type", "system") val source = subject.system() it("should have correct type") { assertThat(source.info["type"], equalTo("system-properties")) } it("should return a source which contains value from system properties") { assertThat(source["type"].asValue(), equalTo("system")) } } on("create source from an empty file") { val file = tempFileOf("") it("should return an empty source") { assertThat( subject.file(file).tree.children, equalTo(mutableMapOf()) ) } } } }) object PropertiesProviderInJavaSpec : SubjectSpek({ subject { PropertiesProvider.get() } itBehavesLike(PropertiesProviderSpec) }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/properties/PropertiesSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.properties import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.ConfigForLoad import com.uchuhimo.konf.source.base.FlatConfigForLoad import com.uchuhimo.konf.source.base.FlatSourceLoadBaseSpec import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object PropertiesSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) }.from.properties.resource("source/source.properties") } itBehavesLike(FlatSourceLoadBaseSpec) }) object PropertiesSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) }.from.properties.resource("source/source.properties") val properties = config.toProperties.toText() Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) }.from.properties.string(properties) } itBehavesLike(FlatSourceLoadBaseSpec) }) ================================================ FILE: konf-core/src/test/kotlin/com/uchuhimo/konf/source/serializer/PrimitiveStdSerializerSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.serializer import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.json.toJson import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object PrimitiveStdSerializerSpec : SubjectSpek({ subject { Config { addSpec(WrappedStringSpec) mapper.registerModule( SimpleModule().apply { addSerializer(WrappedString::class.java, WrappedStringStdSerializer()) addDeserializer(WrappedString::class.java, WrappedStringStdDeserializer()) } ) } } given("a config") { val json = """ { "wrapped-string" : "1234" } """.trimIndent().replace("\n", System.lineSeparator()) on("write wrapped string to json") { subject[WrappedStringSpec.wrappedString] = WrappedString("1234") val result = subject.toJson.toText() it("should serialize wrapped string as string") { assertThat(result, equalTo(json)) } } on("read wrapped string from json") { val config = subject.from.json.string(json) it("should deserialize wrapped string from string") { assertThat(config[WrappedStringSpec.wrappedString], equalTo(WrappedString("1234"))) } } } }) private object WrappedStringSpec : ConfigSpec("") { val wrappedString by optional(name = "wrapped-string", default = WrappedString("value")) } private class WrappedStringStdSerializer : StdSerializer(WrappedString::class.java) { override fun serialize(value: WrappedString, gen: JsonGenerator, provider: SerializerProvider) { gen.writeString(value.string) } } private class WrappedStringStdDeserializer : StdDeserializer(WrappedString::class.java) { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): WrappedString { return WrappedString(p.valueAsString) } } private data class WrappedString(val string: String) ================================================ FILE: konf-core/src/test/resources/source/provider.properties ================================================ type=resource ================================================ FILE: konf-core/src/test/resources/source/source.json ================================================ { "level1": { "level2": { "empty": "null", "literalEmpty": null, "present": 1, "boolean": false, "int": 1, "short": 2, "byte": 3, "bigInteger": 4, "long": 4, "double": 1.5, "float": -1.5, "bigDecimal": 1.5, "char": "a", "string": "string", "offsetTime": "10:15:30+01:00", "offsetDateTime": "2007-12-03T10:15:30+01:00", "zonedDateTime": "2007-12-03T10:15:30+01:00[Europe/Paris]", "localDate": "2007-12-03", "localTime": "10:15:30", "localDateTime": "2007-12-03T10:15:30", "date": "2007-12-03T10:15:30Z", "year": "2007", "yearMonth": "2007-12", "instant": "2007-12-03T10:15:30.00Z", "duration": "P2DT3H4M", "simpleDuration": "200millis", "size": "10k", "enum": "LABEL2", "array": { "boolean": [ true, false ], "byte": [ 1, 2, 3 ], "short": [ 1, 2, 3 ], "int": [ 1, 2, 3 ], "long": [ 4, 5, 6 ], "float": [ -1.0, 0.0, 1.0 ], "double": [ -1.0, 0.0, 1.0 ], "char": [ "a", "b", "c" ], "object": { "boolean": [ true, false ], "int": [ 1, 2, 3 ], "string": [ "one", "two", "three" ], "enum": [ "LABEL1", "LABEL2", "LABEL3" ] } }, "list": [ 1, 2, 3 ], "mutableList": [ 1, 2, 3 ], "listOfList": [ [ 1, 2 ], [ 3, 4 ] ], "set": [ 1, 2, 1 ], "sortedSet": [ 2, 1, 1, 3 ], "map": { "a": 1, "b": 2, "c": 3 }, "intMap": { "1": "a", "2": "b", "3": "c" }, "sortedMap": { "c": 3, "b": 2, "a": 1 }, "listOfMap": [ { "a": 1, "b": 2 }, { "a": 3, "b": 4 } ], "nested": [ [ [ { "a": 1 } ] ] ], "pair": { "first": 1, "second": 2 }, "clazz": { "empty": "null", "literalEmpty": null, "present": 1, "boolean": false, "int": 1, "short": 2, "byte": 3, "bigInteger": 4, "long": 4, "double": 1.5, "float": -1.5, "bigDecimal": 1.5, "char": "a", "string": "string", "offsetTime": "10:15:30+01:00", "offsetDateTime": "2007-12-03T10:15:30+01:00", "zonedDateTime": "2007-12-03T10:15:30+01:00[Europe/Paris]", "localDate": "2007-12-03", "localTime": "10:15:30", "localDateTime": "2007-12-03T10:15:30", "date": "2007-12-03T10:15:30Z", "year": "2007", "yearMonth": "2007-12", "instant": "2007-12-03T10:15:30.00Z", "duration": "P2DT3H4M", "simpleDuration": "200millis", "size": "10k", "enum": "LABEL2", "booleanArray": [ true, false ], "nested": [ [ [ { "a": 1 } ] ] ] } } } } ================================================ FILE: konf-core/src/test/resources/source/source.properties ================================================ level1.level2.empty=null level1.level2.literalEmpty=null level1.level2.present=1 level1.level2.boolean=false level1.level2.int=1 level1.level2.short=2 level1.level2.byte=3 level1.level2.bigInteger=4 level1.level2.long=4 level1.level2.double=1.5 level1.level2.float=-1.5 level1.level2.bigDecimal=1.5 level1.level2.char=a level1.level2.string=string level1.level2.offsetTime=10:15:30+01:00 level1.level2.offsetDateTime=2007-12-03T10:15:30+01:00 level1.level2.zonedDateTime=2007-12-03T10:15:30+01:00[Europe/Paris] level1.level2.localDate=2007-12-03 level1.level2.localTime=10:15:30 level1.level2.localDateTime=2007-12-03T10:15:30 level1.level2.date=2007-12-03T10:15:30Z level1.level2.year=2007 level1.level2.yearMonth=2007-12 level1.level2.instant=2007-12-03T10:15:30.00Z level1.level2.duration=P2DT3H4M level1.level2.simpleDuration=200millis level1.level2.size=10k level1.level2.enum=LABEL2 level1.level2.list=1,2,3 level1.level2.mutableList=1,2,3 level1.level2.listOfList.0=1,2 level1.level2.listOfList.1=3,4 level1.level2.set=1,2,1 level1.level2.sortedSet=2,1,1,3 level1.level2.map.a=1 level1.level2.map.b=2 level1.level2.map.c=3 level1.level2.intMap.1=a level1.level2.intMap.2=b level1.level2.intMap.3=c level1.level2.sortedMap.c=3 level1.level2.sortedMap.b=2 level1.level2.sortedMap.a=1 level1.level2.nested.0.0.0.a=1 level1.level2.listOfMap.0.a=1 level1.level2.listOfMap.0.b=2 level1.level2.listOfMap.1.a=3 level1.level2.listOfMap.1.b=4 level1.level2.array.boolean=true,false level1.level2.array.byte=1,2,3 level1.level2.array.short=1,2,3 level1.level2.array.int=1,2,3 level1.level2.array.long=4,5,6 level1.level2.array.float=-1, 0.0, 1 level1.level2.array.double=-1, 0.0, 1 level1.level2.array.char=a,b,c level1.level2.array.object.boolean=true,false level1.level2.array.object.int=1,2,3 level1.level2.array.object.string=one,two,three level1.level2.array.object.enum=LABEL1,LABEL2,LABEL3 level1.level2.pair.first=1 level1.level2.pair.second=2 level1.level2.clazz.empty=null level1.level2.clazz.literalEmpty=null level1.level2.clazz.present=1 level1.level2.clazz.boolean=false level1.level2.clazz.int=1 level1.level2.clazz.short=2 level1.level2.clazz.byte=3 level1.level2.clazz.bigInteger=4 level1.level2.clazz.long=4 level1.level2.clazz.double=1.5 level1.level2.clazz.float=-1.5 level1.level2.clazz.bigDecimal=1.5 level1.level2.clazz.char=a level1.level2.clazz.string=string level1.level2.clazz.offsetTime=10:15:30+01:00 level1.level2.clazz.offsetDateTime=2007-12-03T10:15:30+01:00 level1.level2.clazz.zonedDateTime=2007-12-03T10:15:30+01:00[Europe/Paris] level1.level2.clazz.localDate=2007-12-03 level1.level2.clazz.localTime=10:15:30 level1.level2.clazz.localDateTime=2007-12-03T10:15:30 level1.level2.clazz.date=2007-12-03T10:15:30Z level1.level2.clazz.year=2007 level1.level2.clazz.yearMonth=2007-12 level1.level2.clazz.instant=2007-12-03T10:15:30.00Z level1.level2.clazz.duration=P2DT3H4M level1.level2.clazz.simpleDuration=200millis level1.level2.clazz.size=10k level1.level2.clazz.enum=LABEL2 level1.level2.clazz.booleanArray=true,false level1.level2.clazz.nested.0.0.0.a=1 level1.level2.emptyList= level1.level2.emptySet= level1.level2.emptyArray= level1.level2.emptyObjectArray= level1.level2.singleElementList=1 level1.level2.multipleElementsList=1,2 level1.level2.flatClass.stringWithComma=string,with,comma level1.level2.flatClass.emptyList= level1.level2.flatClass.emptySet= level1.level2.flatClass.emptyArray= level1.level2.flatClass.emptyObjectArray= level1.level2.flatClass.singleElementList=1 level1.level2.flatClass.multipleElementsList=1,2 ================================================ FILE: konf-core/src/testFixtures/kotlin/com/uchuhimo/konf/TestUtils.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf import java.io.File fun tempFileOf(content: String, prefix: String = "tmp", suffix: String = ".tmp"): File { return tempFile(prefix, suffix).apply { writeText(content) } } ================================================ FILE: konf-core/src/testFixtures/kotlin/com/uchuhimo/konf/source/ConfigForLoad.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.SizeInBytes import java.io.Serializable import java.math.BigDecimal import java.math.BigInteger import java.time.Duration import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.OffsetDateTime import java.time.OffsetTime import java.time.Year import java.time.YearMonth import java.time.ZonedDateTime import java.util.Date import java.util.SortedMap import java.util.SortedSet object ConfigForLoad : ConfigSpec("level1.level2") { val empty by required() val literalEmpty by required() val present by required() val boolean by required() val int by required() val short by required() val byte by required() val bigInteger by required() val long by required() val double by required() val float by required() val bigDecimal by required() val char by required() val string by required() val offsetTime by required() val offsetDateTime by required() val zonedDateTime by required() val localDate by required() val localTime by required() val localDateTime by required() val date by required() val year by required() val yearMonth by required() val instant by required() val duration by required() val simpleDuration by required() val size by required() val enum by required() // array items val booleanArray by required("array.boolean") val byteArray by required("array.byte") val shortArray by required("array.short") val intArray by required("array.int") val longArray by required("array.long") val floatArray by required("array.float") val doubleArray by required("array.double") val charArray by required("array.char") // object array item val booleanObjectArray by required>("array.object.boolean") val intObjectArray by required>("array.object.int") val stringArray by required>("array.object.string") val enumArray by required>("array.object.enum") val list by required>() val mutableList by required>() val listOfList by required>>() val set by required>() val sortedSet by required>() val map by required>() val intMap by required>() val sortedMap by required>() val listOfMap by required>>() val nested by required>>>>() val pair by required>() val clazz by required() } enum class EnumForLoad { LABEL1, LABEL2, LABEL3 } data class ClassForLoad( val empty: Int?, val literalEmpty: Int?, val present: Int?, val boolean: Boolean, val int: Int, val short: Short, val byte: Byte, val bigInteger: BigInteger, val long: Long, val double: Double, val float: Float, val bigDecimal: BigDecimal, val char: Char, val string: String, val offsetTime: OffsetTime, val offsetDateTime: OffsetDateTime, val zonedDateTime: ZonedDateTime, val localDate: LocalDate, val localTime: LocalTime, val localDateTime: LocalDateTime, val date: Date, val year: Year, val yearMonth: YearMonth, val instant: Instant, val duration: Duration, val simpleDuration: Duration, val size: SizeInBytes, val enum: EnumForLoad, val booleanArray: BooleanArray, val nested: Array>>> ) : Serializable ================================================ FILE: konf-core/src/testFixtures/kotlin/com/uchuhimo/konf/source/SingleThreadDispatcher.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asCoroutineDispatcher import java.util.concurrent.Executors fun newSequentialDispatcher() = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val dispatcher = newSequentialDispatcher() val Dispatchers.Sequential: CoroutineDispatcher get() = dispatcher ================================================ FILE: konf-core/src/testFixtures/kotlin/com/uchuhimo/konf/source/SourceLoadBaseSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.SizeInBytes import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.math.BigDecimal import java.math.BigInteger import java.time.Duration import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.OffsetDateTime import java.time.OffsetTime import java.time.Year import java.time.YearMonth import java.time.ZonedDateTime import java.util.Arrays import java.util.Date import java.util.SortedSet import kotlin.test.assertNull import kotlin.test.assertTrue object SourceLoadBaseSpec : SubjectSpek({ given("a source") { on("load the source into config") { it("should contain every value specified in the source") { assertNull(subject[ConfigForLoad.empty]) assertNull(subject[ConfigForLoad.literalEmpty]) assertThat(subject[ConfigForLoad.present], equalTo(1)) assertThat(subject[ConfigForLoad.boolean], equalTo(false)) assertThat(subject[ConfigForLoad.int], equalTo(1)) assertThat(subject[ConfigForLoad.short], equalTo(2.toShort())) assertThat(subject[ConfigForLoad.byte], equalTo(3.toByte())) assertThat(subject[ConfigForLoad.bigInteger], equalTo(BigInteger.valueOf(4))) assertThat(subject[ConfigForLoad.long], equalTo(4L)) assertThat(subject[ConfigForLoad.double], equalTo(1.5)) assertThat(subject[ConfigForLoad.float], equalTo(-1.5f)) assertThat(subject[ConfigForLoad.bigDecimal], equalTo(BigDecimal.valueOf(1.5))) assertThat(subject[ConfigForLoad.char], equalTo('a')) assertThat(subject[ConfigForLoad.string], equalTo("string")) assertThat( subject[ConfigForLoad.offsetTime], equalTo(OffsetTime.parse("10:15:30+01:00")) ) assertThat( subject[ConfigForLoad.offsetDateTime], equalTo(OffsetDateTime.parse("2007-12-03T10:15:30+01:00")) ) assertThat( subject[ConfigForLoad.zonedDateTime], equalTo(ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]")) ) assertThat( subject[ConfigForLoad.localDate], equalTo(LocalDate.parse("2007-12-03")) ) assertThat( subject[ConfigForLoad.localTime], equalTo(LocalTime.parse("10:15:30")) ) assertThat( subject[ConfigForLoad.localDateTime], equalTo(LocalDateTime.parse("2007-12-03T10:15:30")) ) assertThat( subject[ConfigForLoad.date], equalTo(Date.from(Instant.parse("2007-12-03T10:15:30Z"))) ) assertThat( subject[ConfigForLoad.year], equalTo(Year.parse("2007")) ) assertThat( subject[ConfigForLoad.yearMonth], equalTo(YearMonth.parse("2007-12")) ) assertThat( subject[ConfigForLoad.instant], equalTo(Instant.parse("2007-12-03T10:15:30.00Z")) ) assertThat( subject[ConfigForLoad.duration], equalTo(Duration.parse("P2DT3H4M")) ) assertThat( subject[ConfigForLoad.simpleDuration], equalTo(Duration.ofMillis(200)) ) assertThat(subject[ConfigForLoad.size].bytes, equalTo(10240L)) assertThat(subject[ConfigForLoad.enum], equalTo(EnumForLoad.LABEL2)) // array items assertTrue( Arrays.equals( subject[ConfigForLoad.booleanArray], booleanArrayOf(true, false) ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.byteArray], byteArrayOf(1, 2, 3) ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.shortArray], shortArrayOf(1, 2, 3) ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.intArray], intArrayOf(1, 2, 3) ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.longArray], longArrayOf(4, 5, 6) ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.floatArray], floatArrayOf(-1.0F, 0.0F, 1.0F) ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.doubleArray], doubleArrayOf(-1.0, 0.0, 1.0) ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.charArray], charArrayOf('a', 'b', 'c') ) ) // object array items assertTrue( Arrays.equals( subject[ConfigForLoad.booleanObjectArray], arrayOf(true, false) ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.intObjectArray], arrayOf(1, 2, 3) ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.stringArray], arrayOf("one", "two", "three") ) ) assertTrue( Arrays.equals( subject[ConfigForLoad.enumArray], arrayOf(EnumForLoad.LABEL1, EnumForLoad.LABEL2, EnumForLoad.LABEL3) ) ) assertThat(subject[ConfigForLoad.list], equalTo(listOf(1, 2, 3))) assertTrue( Arrays.equals( subject[ConfigForLoad.mutableList].toTypedArray(), arrayOf(1, 2, 3) ) ) assertThat( subject[ConfigForLoad.listOfList], equalTo(listOf(listOf(1, 2), listOf(3, 4))) ) assertThat(subject[ConfigForLoad.set], equalTo(setOf(1, 2))) assertThat( subject[ConfigForLoad.sortedSet], equalTo>(sortedSetOf(1, 2, 3)) ) assertThat( subject[ConfigForLoad.map], equalTo(mapOf("a" to 1, "b" to 2, "c" to 3)) ) assertThat( subject[ConfigForLoad.intMap], equalTo(mapOf(1 to "a", 2 to "b", 3 to "c")) ) assertThat( subject[ConfigForLoad.sortedMap], equalTo(sortedMapOf("a" to 1, "b" to 2, "c" to 3)) ) assertThat(subject[ConfigForLoad.sortedMap].firstKey(), equalTo("a")) assertThat(subject[ConfigForLoad.sortedMap].lastKey(), equalTo("c")) assertThat( subject[ConfigForLoad.listOfMap], equalTo(listOf(mapOf("a" to 1, "b" to 2), mapOf("a" to 3, "b" to 4))) ) assertTrue( Arrays.equals( subject[ConfigForLoad.nested], arrayOf(listOf(setOf(mapOf("a" to 1)))) ) ) assertThat(subject[ConfigForLoad.pair], equalTo(1 to 2)) val classForLoad = ClassForLoad( empty = null, literalEmpty = null, present = 1, boolean = false, int = 1, short = 2.toShort(), byte = 3.toByte(), bigInteger = BigInteger.valueOf(4), long = 4L, double = 1.5, float = -1.5f, bigDecimal = BigDecimal.valueOf(1.5), char = 'a', string = "string", offsetTime = OffsetTime.parse("10:15:30+01:00"), offsetDateTime = OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), zonedDateTime = ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), localDate = LocalDate.parse("2007-12-03"), localTime = LocalTime.parse("10:15:30"), localDateTime = LocalDateTime.parse("2007-12-03T10:15:30"), date = Date.from(Instant.parse("2007-12-03T10:15:30Z")), year = Year.parse("2007"), yearMonth = YearMonth.parse("2007-12"), instant = Instant.parse("2007-12-03T10:15:30.00Z"), duration = "P2DT3H4M".toDuration(), simpleDuration = Duration.ofMillis(200), size = SizeInBytes.parse("10k"), enum = EnumForLoad.LABEL2, booleanArray = booleanArrayOf(true, false), nested = arrayOf(listOf(setOf(mapOf("a" to 1)))) ) assertThat(subject[ConfigForLoad.clazz].empty, equalTo(classForLoad.empty)) assertThat(subject[ConfigForLoad.clazz].literalEmpty, equalTo(classForLoad.literalEmpty)) assertThat(subject[ConfigForLoad.clazz].present, equalTo(classForLoad.present)) assertThat(subject[ConfigForLoad.clazz].boolean, equalTo(classForLoad.boolean)) assertThat(subject[ConfigForLoad.clazz].int, equalTo(classForLoad.int)) assertThat(subject[ConfigForLoad.clazz].short, equalTo(classForLoad.short)) assertThat(subject[ConfigForLoad.clazz].byte, equalTo(classForLoad.byte)) assertThat(subject[ConfigForLoad.clazz].bigInteger, equalTo(classForLoad.bigInteger)) assertThat(subject[ConfigForLoad.clazz].long, equalTo(classForLoad.long)) assertThat(subject[ConfigForLoad.clazz].double, equalTo(classForLoad.double)) assertThat(subject[ConfigForLoad.clazz].float, equalTo(classForLoad.float)) assertThat(subject[ConfigForLoad.clazz].bigDecimal, equalTo(classForLoad.bigDecimal)) assertThat(subject[ConfigForLoad.clazz].char, equalTo(classForLoad.char)) assertThat(subject[ConfigForLoad.clazz].string, equalTo(classForLoad.string)) assertThat(subject[ConfigForLoad.clazz].offsetTime, equalTo(classForLoad.offsetTime)) assertThat(subject[ConfigForLoad.clazz].offsetDateTime, equalTo(classForLoad.offsetDateTime)) assertThat(subject[ConfigForLoad.clazz].zonedDateTime, equalTo(classForLoad.zonedDateTime)) assertThat(subject[ConfigForLoad.clazz].localDate, equalTo(classForLoad.localDate)) assertThat(subject[ConfigForLoad.clazz].localTime, equalTo(classForLoad.localTime)) assertThat(subject[ConfigForLoad.clazz].localDateTime, equalTo(classForLoad.localDateTime)) assertThat(subject[ConfigForLoad.clazz].date, equalTo(classForLoad.date)) assertThat(subject[ConfigForLoad.clazz].year, equalTo(classForLoad.year)) assertThat(subject[ConfigForLoad.clazz].yearMonth, equalTo(classForLoad.yearMonth)) assertThat(subject[ConfigForLoad.clazz].instant, equalTo(classForLoad.instant)) assertThat(subject[ConfigForLoad.clazz].duration, equalTo(classForLoad.duration)) assertThat(subject[ConfigForLoad.clazz].simpleDuration, equalTo(classForLoad.simpleDuration)) assertThat(subject[ConfigForLoad.clazz].size, equalTo(classForLoad.size)) assertThat(subject[ConfigForLoad.clazz].enum, equalTo(classForLoad.enum)) assertTrue(Arrays.equals(subject[ConfigForLoad.clazz].booleanArray, classForLoad.booleanArray)) assertTrue(Arrays.equals(subject[ConfigForLoad.clazz].nested, classForLoad.nested)) } } } }) ================================================ FILE: konf-core/src/testFixtures/kotlin/com/uchuhimo/konf/source/TestUtils.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.Matcher import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.has import com.natpryce.hamkrest.isA import com.natpryce.hamkrest.throws import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec object DefaultLoadersConfig : ConfigSpec("source.test") { val type by required() } fun Source.toConfig(): Config = Config { addSpec(DefaultLoadersConfig) }.withSource(this) inline fun assertCausedBy(noinline block: () -> Unit) { @Suppress("UNCHECKED_CAST") assertThat( block, throws( has( LoadException::cause, isA() as Matcher ) ) ) } const val propertiesContent = "source.test.type = properties" ================================================ FILE: konf-core/src/testFixtures/kotlin/com/uchuhimo/konf/source/base/FlatConfigForLoad.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.uchuhimo.konf.ConfigSpec import java.io.Serializable object FlatConfigForLoad : ConfigSpec("level1.level2") { val emptyList by required>() val emptySet by required>() val emptyArray by required() val emptyObjectArray by required>() val singleElementList by required>() val multipleElementsList by required>() val flatClass by required() } data class ClassForLoad( val stringWithComma: String, val emptyList: List, val emptySet: Set, val emptyArray: IntArray, val emptyObjectArray: Array, val singleElementList: List, val multipleElementsList: List ) : Serializable ================================================ FILE: konf-core/src/testFixtures/kotlin/com/uchuhimo/konf/source/base/FlatSourceLoadBaseSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.base import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.SourceLoadBaseSpec import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike import java.util.Arrays import kotlin.test.assertTrue object FlatSourceLoadBaseSpec : SubjectSpek({ itBehavesLike(SourceLoadBaseSpec) given("a flat source") { on("load the source into config") { it("should contain every value specified in the source") { val classForLoad = ClassForLoad( stringWithComma = "string,with,comma", emptyList = listOf(), emptySet = setOf(), emptyArray = intArrayOf(), emptyObjectArray = arrayOf(), singleElementList = listOf(1), multipleElementsList = listOf(1, 2) ) assertThat(subject[FlatConfigForLoad.emptyList], equalTo(listOf())) assertThat(subject[FlatConfigForLoad.emptySet], equalTo(setOf())) assertTrue(Arrays.equals(subject[FlatConfigForLoad.emptyArray], intArrayOf())) assertTrue(Arrays.equals(subject[FlatConfigForLoad.emptyObjectArray], arrayOf())) assertThat(subject[FlatConfigForLoad.singleElementList], equalTo(listOf(1))) assertThat(subject[FlatConfigForLoad.multipleElementsList], equalTo(listOf(1, 2))) assertThat( subject[FlatConfigForLoad.flatClass].stringWithComma, equalTo(classForLoad.stringWithComma) ) } } } }) ================================================ FILE: konf-git/build.gradle.kts ================================================ dependencies { api(project(":konf-core")) api("org.eclipse.jgit", "org.eclipse.jgit", Versions.jgit) testImplementation(testFixtures(project(":konf-core"))) } ================================================ FILE: konf-git/src/main/kotlin/com/uchuhimo/konf/source/DefaultGitLoader.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.Config import kotlinx.coroutines.Dispatchers import org.eclipse.jgit.lib.Constants import java.io.File import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext /** * Returns a child config containing values from a specified git repository. * * Format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the url extension is unsupported. * * @param repo git repository * @param file file in the git repository * @param dir local directory of the git repository * @param branch the initial branch * @param optional whether the source is optional * @param action additional action when cloning/pulling * @return a child config containing values from a specified git repository * @throws UnsupportedExtensionException */ fun DefaultLoaders.git( repo: String, file: String, dir: String? = null, branch: String = Constants.HEAD, optional: Boolean = this.optional ): Config = dispatchExtension(File(file).extension, "{repo: $repo, file: $file}") .git(repo, file, dir, branch, optional) /** * Returns a child config containing values from a specified git repository, * and reloads values periodically. * * Format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the url extension is unsupported. * * @param repo git repository * @param file file in the git repository * @param dir local directory of the git repository * @param branch the initial branch * @param period reload period. The default value is 1. * @param unit time unit of reload period. The default value is [TimeUnit.MINUTES]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated git file is loaded * @return a child config containing values from a specified git repository * @throws UnsupportedExtensionException */ fun DefaultLoaders.watchGit( repo: String, file: String, dir: String? = null, branch: String = Constants.HEAD, period: Long = 1, unit: TimeUnit = TimeUnit.MINUTES, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config = dispatchExtension(File(file).extension, "{repo: $repo, file: $file}") .watchGit(repo, file, dir, branch, period, unit, context, optional, onLoad) ================================================ FILE: konf-git/src/main/kotlin/com/uchuhimo/konf/source/DefaultGitProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import org.eclipse.jgit.lib.Constants import java.io.File /** * Returns a source from a specified git repository. * * Format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json * - Properties: properties * - TOML: toml * - XML: xml * - YAML: yml, yaml * * Throws [UnsupportedExtensionException] if the url extension is unsupported. * * @param repo git repository * @param file file in the git repository * @param dir local directory of the git repository * @param branch the initial branch * @param optional whether the source is optional * @return a source from a specified git repository * @throws UnsupportedExtensionException */ fun DefaultProviders.git( repo: String, file: String, dir: String? = null, branch: String = Constants.HEAD, optional: Boolean = false ): Source = dispatchExtension(File(file).extension, "{repo: $repo, file: $file}") .git(repo, file, dir, branch, optional) ================================================ FILE: konf-git/src/main/kotlin/com/uchuhimo/konf/source/GitLoader.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.Config import com.uchuhimo.konf.tempDirectory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.eclipse.jgit.lib.Constants import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext /** * Returns a child config containing values from a specified git repository. * * @param repo git repository * @param file file in the git repository * @param dir local directory of the git repository * @param branch the initial branch * @param optional whether the source is optional * @return a child config containing values from a specified git repository */ fun Loader.git( repo: String, file: String, dir: String? = null, branch: String = Constants.HEAD, optional: Boolean = this.optional ): Config = config.withSource(provider.git(repo, file, dir, branch, optional)) /** * Returns a child config containing values from a specified git repository, * and reloads values periodically. * * @param repo git repository * @param file file in the git repository * @param dir local directory of the git repository * @param branch the initial branch * @param period reload period. The default value is 1. * @param unit time unit of reload period. The default value is [TimeUnit.MINUTES]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated git file is loaded * @return a child config containing values from a specified git repository */ fun Loader.watchGit( repo: String, file: String, dir: String? = null, branch: String = Constants.HEAD, period: Long = 1, unit: TimeUnit = TimeUnit.MINUTES, context: CoroutineContext = Dispatchers.Default, optional: Boolean = this.optional, onLoad: ((config: Config, source: Source) -> Unit)? = null ): Config { return (dir ?: tempDirectory(prefix = "local_git_repo").path).let { directory -> provider.git(repo, file, directory, branch, optional).let { source -> config.withLoadTrigger("watch ${source.description}") { newConfig, load -> newConfig.lock { load(source) } onLoad?.invoke(newConfig, source) GlobalScope.launch(context) { while (true) { delay(unit.toMillis(period)) val newSource = provider.git(repo, file, directory, branch, optional) newConfig.lock { newConfig.clear() load(newSource) } onLoad?.invoke(newConfig, newSource) } } }.withLayer() } } } ================================================ FILE: konf-git/src/main/kotlin/com/uchuhimo/konf/source/GitProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.base.EmptyMapSource import com.uchuhimo.konf.tempDirectory import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.errors.GitAPIException import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.transport.URIish import java.io.File import java.io.IOException import java.nio.file.Paths /** * Returns a new source from a specified git repository. * * @param repo git repository * @param file file in the git repository * @param dir local directory of the git repository * @param branch the initial branch * @param optional whether this source is optional * @return a new source from a specified git repository */ fun Provider.git( repo: String, file: String, dir: String? = null, branch: String = Constants.HEAD, optional: Boolean = false ): Source { return (dir?.let(::File) ?: tempDirectory(prefix = "local_git_repo")).let { directory -> val extendContext: Source.() -> Unit = { info["repo"] = repo info["file"] = file info["dir"] = directory.path info["branch"] = branch } try { if ((directory.list { _, name -> name == ".git" } ?: emptyArray()).isEmpty()) { Git.cloneRepository().apply { setURI(repo) setDirectory(directory) setBranch(branch) }.call().close() } else { Git.open(directory).use { git -> val uri = URIish(repo) val remoteName = git.remoteList().call().firstOrNull { it.urIs.contains(uri) }?.name ?: throw InvalidRemoteRepoException(repo, directory.path) git.pull().apply { remote = remoteName remoteBranchName = branch }.call() } } } catch (ex: Exception) { when (ex) { is GitAPIException, is IOException, is SourceException -> { if (optional) { return EmptyMapSource().apply(extendContext) } else { throw ex } } else -> throw ex } } file(Paths.get(directory.path, file).toFile(), optional).apply(extendContext) } } ================================================ FILE: konf-git/src/test/kotlin/com/uchuhimo/konf/source/DefaultGitLoaderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.tempDirectory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Constants import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.nio.file.Paths import java.util.concurrent.TimeUnit object DefaultGitLoaderSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig) }.from } val item = DefaultLoadersConfig.type given("a loader") { on("load from git repository") { tempDirectory().let { dir -> Git.init().apply { setDirectory(dir) }.call().use { git -> Paths.get(dir.path, "source.properties").toFile().writeText(propertiesContent) git.add().apply { addFilepattern("source.properties") }.call() git.commit().apply { message = "init commit" }.call() } val repo = dir.toURI() val config = subject.git(repo.toString(), "source.properties") it("should load as auto-detected file format") { assertThat(config[item], equalTo("properties")) } } } mapOf( "load from watched git repository" to { loader: DefaultLoaders, repo: String -> loader.watchGit( repo, "source.properties", period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential ) }, "load from watched git repository to the given directory" to { loader: DefaultLoaders, repo: String -> loader.watchGit( repo, "source.properties", dir = tempDirectory(prefix = "local_git_repo").path, branch = Constants.HEAD, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential, optional = false ) } ).forEach { (description, func) -> on(description) { tempDirectory(prefix = "remote_git_repo", suffix = ".git").let { dir -> val file = Paths.get(dir.path, "source.properties").toFile() Git.init().apply { setDirectory(dir) }.call().use { git -> file.writeText(propertiesContent) git.add().apply { addFilepattern("source.properties") }.call() git.commit().apply { message = "init commit" }.call() } val repo = dir.toURI() val config = func(subject, repo.toString()) val originalValue = config[item] file.writeText(propertiesContent.replace("properties", "newValue")) Git.open(dir).use { git -> git.add().apply { addFilepattern("source.properties") }.call() git.commit().apply { message = "update value" }.call() } runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[item] it("should load as auto-detected file format") { assertThat(originalValue, equalTo("properties")) } it("should load new value after file content in git repository has been changed") { assertThat(newValue, equalTo("newValue")) } } } } on("load from watched git repository with listener") { tempDirectory(prefix = "remote_git_repo", suffix = ".git").let { dir -> val file = Paths.get(dir.path, "source.properties").toFile() Git.init().apply { setDirectory(dir) }.call().use { git -> file.writeText(propertiesContent) git.add().apply { addFilepattern("source.properties") }.call() git.commit().apply { message = "init commit" }.call() } val repo = dir.toURI() var newValue = "" val config = subject.watchGit( repo.toString(), "source.properties", period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential ) { config, _ -> newValue = config[item] } val originalValue = config[item] file.writeText(propertiesContent.replace("properties", "newValue")) Git.open(dir).use { git -> git.add().apply { addFilepattern("source.properties") }.call() git.commit().apply { message = "update value" }.call() } runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } it("should load as auto-detected file format") { assertThat(originalValue, equalTo("properties")) } it("should load new value after file content in git repository has been changed") { assertThat(newValue, equalTo("newValue")) } } } } }) ================================================ FILE: konf-git/src/test/kotlin/com/uchuhimo/konf/source/DefaultGitProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.tempDirectory import org.eclipse.jgit.api.Git import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.nio.file.Paths object DefaultGitProviderSpec : SubjectSpek({ subject { Source.from } val item = DefaultLoadersConfig.type given("a provider") { on("provider source from git repository") { tempDirectory().let { dir -> Git.init().apply { setDirectory(dir) }.call().use { git -> Paths.get(dir.path, "source.properties").toFile().writeText(propertiesContent) git.add().apply { addFilepattern("source.properties") }.call() git.commit().apply { message = "init commit" }.call() } val repo = dir.toURI() val config = subject.git(repo.toString(), "source.properties").toConfig() it("should provide as auto-detected file format") { assertThat(config[item], equalTo("properties")) } } } } }) ================================================ FILE: konf-git/src/test/kotlin/com/uchuhimo/konf/source/GitLoaderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.properties.PropertiesProvider import com.uchuhimo.konf.tempDirectory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Constants import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.nio.file.Paths import java.util.concurrent.TimeUnit object GitLoaderSpec : SubjectSpek({ val parentConfig = Config { addSpec(SourceType) } subject { Loader(parentConfig, PropertiesProvider) } given("a loader") { on("load from git repository") { tempDirectory().let { dir -> Git.init().apply { setDirectory(dir) }.call().use { git -> Paths.get(dir.path, "test").toFile().writeText("type = git") git.add().apply { addFilepattern("test") }.call() git.commit().apply { message = "init commit" }.call() } val repo = dir.toURI() val config = subject.git(repo.toString(), "test") it("should return a config which contains value in git repository") { assertThat(config[SourceType.type], equalTo("git")) } } } mapOf( "load from watched git repository" to { loader: Loader, repo: String -> loader.watchGit( repo, "test", period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential ) }, "load from watched git repository to the given directory" to { loader: Loader, repo: String -> loader.watchGit( repo, "test", dir = tempDirectory(prefix = "local_git_repo").path, branch = Constants.HEAD, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential, optional = false ) } ).forEach { (description, func) -> on(description) { tempDirectory(prefix = "remote_git_repo", suffix = ".git").let { dir -> val file = Paths.get(dir.path, "test").toFile() Git.init().apply { setDirectory(dir) }.call().use { git -> file.writeText("type = originalValue") git.add().apply { addFilepattern("test") }.call() git.commit().apply { message = "init commit" }.call() } val repo = dir.toURI() val config = func(subject, repo.toString()) val originalValue = config[SourceType.type] file.writeText("type = newValue") Git.open(dir).use { git -> git.add().apply { addFilepattern("test") }.call() git.commit().apply { message = "update value" }.call() } runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } val newValue = config[SourceType.type] it("should return a config which contains value in git repository") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value when content of git repository has been changed") { assertThat(newValue, equalTo("newValue")) } } } } on("load from watched git repository with listener") { tempDirectory(prefix = "remote_git_repo", suffix = ".git").let { dir -> val file = Paths.get(dir.path, "test").toFile() Git.init().apply { setDirectory(dir) }.call().use { git -> file.writeText("type = originalValue") git.add().apply { addFilepattern("test") }.call() git.commit().apply { message = "init commit" }.call() } val repo = dir.toURI() var newValue = "" val config = subject.watchGit( repo.toString(), "test", period = 1, unit = TimeUnit.SECONDS, context = Dispatchers.Sequential ) { config, _ -> newValue = config[SourceType.type] } val originalValue = config[SourceType.type] file.writeText("type = newValue") Git.open(dir).use { git -> git.add().apply { addFilepattern("test") }.call() git.commit().apply { message = "update value" }.call() } runBlocking(Dispatchers.Sequential) { delay(TimeUnit.SECONDS.toMillis(1)) } it("should return a config which contains value in git repository") { assertThat(originalValue, equalTo("originalValue")) } it("should load new value when content of git repository has been changed") { assertThat(newValue, equalTo("newValue")) } } } } }) private object SourceType : ConfigSpec("") { val type by required() } ================================================ FILE: konf-git/src/test/kotlin/com/uchuhimo/konf/source/GitProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.properties.PropertiesProvider import com.uchuhimo.konf.tempDirectory import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Constants import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.nio.file.Paths import kotlin.test.assertTrue object GitProviderSpec : SubjectSpek({ subject { PropertiesProvider } given("a provider") { on("create source from git repository") { tempDirectory().let { dir -> Git.init().apply { setDirectory(dir) }.call().use { git -> Paths.get(dir.path, "test").toFile().writeText("type = git") git.add().apply { addFilepattern("test") }.call() git.commit().apply { message = "init commit" }.call() } val repo = dir.toURI() val source = subject.git(repo.toString(), "test") it("should create from the specified git repository") { assertThat(source.info["repo"], equalTo(repo.toString())) assertThat(source.info["file"], equalTo("test")) assertThat(source.info["branch"], equalTo(Constants.HEAD)) } it("should return a source which contains value in git repository") { assertThat(source["type"].asValue(), equalTo("git")) } } } on("create source from invalid git repository") { tempDirectory().let { dir -> Git.init().apply { setDirectory(dir) }.call().use { git -> Paths.get(dir.path, "test").toFile().writeText("type = git") git.add().apply { addFilepattern("test") }.call() git.commit().apply { message = "init commit" }.call() } it("should throw InvalidRemoteRepoException") { assertThat( { subject.git(tempDirectory().path, "test", dir = dir.path) }, throws() ) } it("should return an empty source if optional") { assertTrue { subject.git(tempDirectory().path, "test", dir = dir.path, optional = true).tree.children.isEmpty() } } } } } }) ================================================ FILE: konf-hocon/build.gradle.kts ================================================ dependencies { api(project(":konf-core")) implementation("com.typesafe", "config", Versions.hocon) testImplementation(testFixtures(project(":konf-core"))) } ================================================ FILE: konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/DefaultHoconLoader.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.hocon.HoconProvider /** * Loader for HOCON source. */ val DefaultLoaders.hocon get() = Loader(config, HoconProvider.orMapped()) ================================================ FILE: konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/DefaultHoconProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.hocon.HoconProvider /** * Provider for HOCON source. */ val DefaultProviders.hocon get() = HoconProvider ================================================ FILE: konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.hocon import com.typesafe.config.ConfigFactory import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.Provider import com.uchuhimo.konf.source.RegisterExtension import com.uchuhimo.konf.source.Source import java.io.InputStream import java.io.Reader /** * Provider for HOCON source. */ @RegisterExtension(["conf"]) object HoconProvider : Provider { override fun reader(reader: Reader): Source = HoconSource(ConfigFactory.parseReader(reader).resolve()) override fun inputStream(inputStream: InputStream): Source { inputStream.reader().use { return reader(it) } } @JavaApi @JvmStatic fun get() = this } ================================================ FILE: konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconSource.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.hocon import com.typesafe.config.Config import com.typesafe.config.ConfigList import com.typesafe.config.ConfigObject import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueType import com.uchuhimo.konf.ContainerNode import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.source.ListSourceNode import com.uchuhimo.konf.source.NullSourceNode import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.SourceInfo import com.uchuhimo.konf.source.ValueSourceNode private fun ConfigValue.toTree(): TreeNode { return when (valueType()!!) { ConfigValueType.NULL -> NullSourceNode ConfigValueType.BOOLEAN, ConfigValueType.NUMBER, ConfigValueType.STRING -> ValueSourceNode(unwrapped()) ConfigValueType.LIST -> ListSourceNode( mutableListOf().apply { for (value in (this@toTree as ConfigList)) { add(value.toTree()) } } ) ConfigValueType.OBJECT -> ContainerNode( mutableMapOf().apply { for ((key, value) in (this@toTree as ConfigObject)) { put(key, value.toTree()) } } ) } } /** * Source from a HOCON value. */ class HoconSource( val value: Config ) : Source { override val info: SourceInfo = SourceInfo("type" to "HOCON") override val tree: TreeNode = value.root().toTree() } ================================================ FILE: konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.hocon import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValueFactory import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.Writer import com.uchuhimo.konf.source.base.toHierarchicalMap import java.io.OutputStream /** * Writer for HOCON source. */ class HoconWriter(val config: Config) : Writer { private val renderOpts = ConfigRenderOptions.defaults() .setOriginComments(false) .setComments(false) .setJson(false) override fun toWriter(writer: java.io.Writer) { writer.write(toText()) } override fun toOutputStream(outputStream: OutputStream) { outputStream.writer().use { toWriter(it) } } override fun toText(): String { return ConfigValueFactory.fromMap(config.toHierarchicalMap()).render(renderOpts) .replace("\n", System.lineSeparator()) } } /** * Returns writer for HOCON source. */ val Config.toHocon: Writer get() = HoconWriter(this) ================================================ FILE: konf-hocon/src/test/java/com/uchuhimo/konf/LoaderJavaApiTest.java ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import com.uchuhimo.konf.source.hocon.HoconProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName("test Java API of loader") class LoaderJavaApiTest { private Config config; @BeforeEach void initConfig() { config = Configs.create(); config.addSpec(NetworkBufferInJava.spec); } @Test @DisplayName("test fluent API to load from default loader") void loadFromDefaultLoader() { final Config newConfig = config .from() .source(HoconProvider.get()) .string(config.nameOf(NetworkBufferInJava.size) + " = 1024"); assertThat(newConfig.get(NetworkBufferInJava.size), equalTo(1024)); } } ================================================ FILE: konf-hocon/src/test/java/com/uchuhimo/konf/NetworkBufferInJava.java ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf; public class NetworkBufferInJava { public static final ConfigSpec spec = new ConfigSpec("network.buffer"); public static final RequiredItem size = new RequiredItem(spec, "size", "size of buffer in KB") {}; } ================================================ FILE: konf-hocon/src/test/kotlin/com/uchuhimo/konf/source/DefaultHoconLoaderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultHoconLoaderSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig) }.from } val item = DefaultLoadersConfig.type given("a loader") { on("load from HOCON file") { val config = subject.file(tempFileOf(hoconContent, suffix = ".conf")) it("should load as auto-detected file format") { assertThat(config[item], equalTo("conf")) } } } }) ================================================ FILE: konf-hocon/src/test/kotlin/com/uchuhimo/konf/source/DefaultHoconProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultHoconProviderSpec : SubjectSpek({ subject { Source.from } val item = DefaultLoadersConfig.type given("a provider") { on("provider source from HOCON file") { val config = subject.file(tempFileOf(hoconContent, suffix = ".conf")).toConfig() it("should provide as auto-detected file format") { assertThat(config[item], equalTo("conf")) } } } }) ================================================ FILE: konf-hocon/src/test/kotlin/com/uchuhimo/konf/source/hocon/HoconProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.hocon import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object HoconProviderSpec : SubjectSpek({ subject { HoconProvider } given("a HOCON provider") { on("create source from reader") { val source = subject.reader("type = reader".reader()) it("should have correct type") { assertThat(source.info["type"], equalTo("HOCON")) } it("should return a source which contains value from reader") { assertThat(source["type"].asValue(), equalTo("reader")) } } on("create source from input stream") { val source = subject.inputStream( tempFileOf("type = inputStream").inputStream() ) it("should have correct type") { assertThat(source.info["type"], equalTo("HOCON")) } it("should return a source which contains value from input stream") { assertThat(source["type"].asValue(), equalTo("inputStream")) } } on("create source from an empty file") { val file = tempFileOf("") it("should return an empty source") { assertThat( subject.file(file).tree.children, equalTo(mutableMapOf()) ) } } } }) object HoconProviderInJavaSpec : SubjectSpek({ subject { HoconProvider.get() } itBehavesLike(HoconProviderSpec) }) ================================================ FILE: konf-hocon/src/test/kotlin/com/uchuhimo/konf/source/hocon/HoconSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.hocon import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.ConfigForLoad import com.uchuhimo.konf.source.SourceLoadBaseSpec import com.uchuhimo.konf.source.hocon import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object HoconSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) enable(Feature.FAIL_ON_UNKNOWN_PATH) }.from.hocon.resource("source/source.conf") } itBehavesLike(SourceLoadBaseSpec) }) object HoconSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) }.from.hocon.resource("source/source.conf") val hocon = config.toHocon.toText() Config { addSpec(ConfigForLoad) }.from.hocon.string(hocon) } itBehavesLike(SourceLoadBaseSpec) }) ================================================ FILE: konf-hocon/src/test/kotlin/com/uchuhimo/konf/source/hocon/HoconSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.hocon import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.asSource import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.toPath import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import kotlin.test.assertNull import kotlin.test.assertTrue object HoconSourceSpec : SubjectSpek({ subject { HoconProvider.string("key = 1") as HoconSource } given("a HOCON source") { on("get underlying config") { it("should return corresponding config") { val config = subject.value assertThat(config.getInt("key"), equalTo(1)) } } on("get an existed key") { it("should contain the key") { assertTrue("key".toPath() in subject) } it("should contain the corresponding value") { assertThat(subject["key".toPath()].asValue(), equalTo(1)) } } on("get an non-existed key") { it("should not contain the key") { assertTrue("invalid".toPath() !in subject) } it("should not contain the corresponding value") { assertNull(subject.getOrNull("invalid".toPath())) } } on("use substitutions in source") { val source = HoconProvider.string( """ key1 = 1 key2 = ${'$'}{key1} """.trimIndent() ) it("should resolve the key") { assertThat(source["key2"].asValue(), equalTo(1)) } } on("use substitutions in source when variables are in other sources") { val source = ( HoconProvider.string( """ key1 = "1" key2 = ${'$'}{key1} key3 = "${'$'}{key4}" key5 = "${'$'}{key1}+${'$'}{key4}" key6 = "${"$$"}{key1}" """.trimIndent() ) + mapOf("key4" to "4", "key1" to "2").asSource() ).substituted().substituted() it("should resolve the key") { assertThat(source["key2"].asValue(), equalTo(1)) assertThat(source["key3"].asValue(), equalTo(4)) assertThat(source["key5"].asValue(), equalTo("2+4")) assertThat(source["key6"].asValue(), equalTo("\${key1}")) } } } }) ================================================ FILE: konf-hocon/src/test/kotlin/com/uchuhimo/konf/source/hocon/HoconValueSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.hocon import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.NoSuchPathException import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.asValue import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import kotlin.test.assertTrue object HoconValueSourceSpec : Spek({ given("a HOCON value source") { on("treat object value source as HOCON source") { val source = "{key = 1}".toHoconValueSource() it("should contain specified value") { assertTrue("key" in source) assertThat(source["key"].asValue(), equalTo(1)) } } on("treat number value source as HOCON source") { val source = "1".toHoconValueSource() it("should throw NoSuchPathException") { assertThat({ source["key"] }, throws()) } } on("get integer from integer value source") { it("should succeed") { assertThat("1".toHoconValueSource().asValue(), equalTo(1)) } } on("get long from long value source") { val source = "123456789000".toHoconValueSource() it("should succeed") { assertThat(source.asValue(), equalTo(123_456_789_000L)) } } on("get long from integer value source") { val source = "1".toHoconValueSource() it("should succeed") { assertThat(source.asValue(), equalTo(1L)) } } on("get double from double value source") { val source = "1.5".toHoconValueSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.5)) } } on("get double from int value source") { val source = "1".toHoconValueSource() it("should succeed") { assertThat(source.asValue(), equalTo(1.0)) } } on("get double from long value source") { val source = "123456789000".toHoconValueSource() it("should succeed") { assertThat(source.asValue(), equalTo(123456789000.0)) } } } }) private fun String.toHoconValueSource(): Source { return HoconProvider.string("key = $this")["key"] } ================================================ FILE: konf-hocon/src/test/kotlin/com/uchuhimo/konf/source/hocon/HoconWriterSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.hocon import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.Writer import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.io.ByteArrayOutputStream import java.io.StringWriter object HoconWriterSpec : SubjectSpek({ subject { val config = Config { addSpec( object : ConfigSpec() { val key by optional("value") } ) } config.toHocon } given("a writer") { val expectedString = "key=value" + System.lineSeparator() on("save to string") { val string = subject.toText() it("should return a string which contains content from config") { assertThat(string, equalTo(expectedString)) } } on("save to writer") { val writer = StringWriter() subject.toWriter(writer) it("should return a writer which contains content from config") { assertThat(writer.toString(), equalTo(expectedString)) } } on("save to output stream") { val outputStream = ByteArrayOutputStream() subject.toOutputStream(outputStream) it("should return an output stream which contains content from config") { assertThat(outputStream.toString(), equalTo(expectedString)) } } } }) ================================================ FILE: konf-hocon/src/test/resources/source/source.conf ================================================ level1 { level2 { empty = "null" literalEmpty = null present = 1 boolean = false int = 1 short = 2 byte = 3 bigInteger = 4 long = 4 double = 1.5 float = -1.5 bigDecimal = 1.5 char = "a" string = string offsetTime = "10:15:30+01:00" offsetDateTime = "2007-12-03T10:15:30+01:00" zonedDateTime = "2007-12-03T10:15:30+01:00[Europe/Paris]" localDate = 2007-12-03 localTime = "10:15:30" localDateTime = "2007-12-03T10:15:30" date = "2007-12-03T10:15:30Z" year = "2007" yearMonth = 2007-12 instant = "2007-12-03T10:15:30.00Z" duration = P2DT3H4M simpleDuration = 200millis size = 10k enum = LABEL2 array { boolean = [true, false] byte = [1, 2, 3] short = [1, 2, 3] int = [1, 2, 3] long = [4, 5, 6] float = [-1, 0.0, 1] double = [-1, 0.0, 1] char = [a, b, c] object { boolean = [true, false] int = [1, 2, 3] string = [one, two, three] enum = [LABEL1, LABEL2, LABEL3] } } list = [1, 2, 3] mutableList = [1, 2, 3] listOfList = [[1, 2], [3, 4]] set = [1, 2, 1] sortedSet = [2, 1, 1, 3] map = {a = 1, b = 2, c = 3} intMap = {1 = a, 2 = b, 3 = c} sortedMap = {c = 3, b = 2, a = 1} listOfMap = [ {a = 1, b = 2} {a = 3, b = 4} ] nested = [[[{a = 1}]]] pair = {first = 1, second = 2} clazz = { empty = "null" literalEmpty = null present = 1 boolean = false int = 1 short = 2 byte = 3 bigInteger = 4 long = 4 double = 1.5 float = -1.5 bigDecimal = 1.5 char = "a" string = string offsetTime = "10:15:30+01:00" offsetDateTime = "2007-12-03T10:15:30+01:00" zonedDateTime = "2007-12-03T10:15:30+01:00[Europe/Paris]" localDate = 2007-12-03 localTime = "10:15:30" localDateTime = "2007-12-03T10:15:30" date = "2007-12-03T10:15:30Z" year = "2007" yearMonth = 2007-12 instant = "2007-12-03T10:15:30.00Z" duration = P2DT3H4M simpleDuration = 200millis size = 10k enum = LABEL2 booleanArray = [true, false] nested = [[[{a = 1}]]] } } } ================================================ FILE: konf-hocon/src/testFixtures/kotlin/com/uchuhimo/konf/source/HoconTestUtils.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source const val hoconContent = "source.test.type = conf" ================================================ FILE: konf-js/build.gradle.kts ================================================ dependencies { api(project(":konf-core")) implementation("org.graalvm.sdk", "graal-sdk", Versions.graal) implementation("org.graalvm.js", "js", Versions.graal) testImplementation(testFixtures(project(":konf-core"))) } ================================================ FILE: konf-js/src/main/kotlin/com/uchuhimo/konf/source/DefaultJsLoader.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.js.JsProvider /** * Loader for JavaScript source. */ val DefaultLoaders.js get() = Loader(config, JsProvider.orMapped()) ================================================ FILE: konf-js/src/main/kotlin/com/uchuhimo/konf/source/DefaultJsProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.js.JsProvider /** * Provider for JavaScript source. */ val DefaultProviders.js get() = JsProvider ================================================ FILE: konf-js/src/main/kotlin/com/uchuhimo/konf/source/js/JsProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.js import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.Provider import com.uchuhimo.konf.source.RegisterExtension import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.json.JsonProvider import org.graalvm.polyglot.Context import java.io.InputStream import java.io.Reader import java.util.stream.Collectors /** * Provider for JavaScript source. */ @RegisterExtension(["js"]) object JsProvider : Provider { override fun reader(reader: Reader): Source { val sourceString = reader.buffered().lines().collect(Collectors.joining("\n")) Context.create().use { context -> val value = context.eval("js", sourceString) context.getBindings("js").putMember("source", value) val jsonString = context.eval("js", "JSON.stringify(source)").asString() return JsonProvider.string(jsonString).apply { this.info["type"] = "JavaScript" } } } override fun inputStream(inputStream: InputStream): Source { inputStream.reader().use { return reader(it) } } @JavaApi @JvmStatic fun get() = this } ================================================ FILE: konf-js/src/main/kotlin/com/uchuhimo/konf/source/js/JsWriter.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.js import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.Writer import com.uchuhimo.konf.source.base.toHierarchicalMap import java.io.OutputStream import java.util.regex.Pattern /** * Writer for JavaScript source. */ class JsWriter(val config: Config) : Writer { override fun toWriter(writer: java.io.Writer) { val jsonOutput = config.mapper .writerWithDefaultPrettyPrinter() .writeValueAsString(config.toHierarchicalMap()) val pattern = Pattern.compile("(\")(.*)(\"\\s*):") val jsOutput = pattern.matcher(jsonOutput).replaceAll("$2:") writer.write("($jsOutput)") } override fun toOutputStream(outputStream: OutputStream) { outputStream.writer().use { toWriter(it) } } } /** * Returns writer for JavaScript source. */ val Config.toJs: Writer get() = JsWriter(this) ================================================ FILE: konf-js/src/test/kotlin/com/uchuhimo/konf/source/DefaultJsLoaderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultJsLoaderSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig) }.from } val item = DefaultLoadersConfig.type given("a loader") { on("load from JavaScript file") { val config = subject.file(tempFileOf(jsContent, suffix = ".js")) it("should load as auto-detected file format") { assertThat(config[item], equalTo("js")) } } } }) //language=JavaScript const val jsContent = """ ({ source: { test: { type: "js" } } }) """ ================================================ FILE: konf-js/src/test/kotlin/com/uchuhimo/konf/source/DefaultJsProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultJsProviderSpec : SubjectSpek({ subject { Source.from } val item = DefaultLoadersConfig.type given("a provider") { on("provider source from JavaScript file") { val config = subject.file(tempFileOf(jsContent, suffix = ".js")).toConfig() it("should provide as auto-detected file format") { assertThat(config[item], equalTo("js")) } } } }) ================================================ FILE: konf-js/src/test/kotlin/com/uchuhimo/konf/source/js/JsProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.js import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object JsProviderSpec : SubjectSpek({ subject { JsProvider } given("a JavaScript provider") { on("create source from reader") { val source = subject.reader("({type: 'reader'})".reader()) it("should have correct type") { assertThat(source.info["type"], equalTo("JavaScript")) } it("should return a source which contains value from reader") { assertThat(source["type"].asValue(), equalTo("reader")) } } on("create source from input stream") { val source = subject.inputStream( tempFileOf("({type: 'inputStream'})").inputStream() ) it("should have correct type") { assertThat(source.info["type"], equalTo("JavaScript")) } it("should return a source which contains value from input stream") { assertThat(source["type"].asValue(), equalTo("inputStream")) } } on("create source from an empty file") { val file = tempFileOf("({})") it("should return an empty source") { assertThat( subject.file(file).tree.children, equalTo(mutableMapOf()) ) } } } }) object JsProviderInJavaSpec : SubjectSpek({ subject { JsProvider.get() } itBehavesLike(JsProviderSpec) }) ================================================ FILE: konf-js/src/test/kotlin/com/uchuhimo/konf/source/js/JsSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.js import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.ConfigForLoad import com.uchuhimo.konf.source.SourceLoadBaseSpec import com.uchuhimo.konf.source.js import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object JsSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) enable(Feature.FAIL_ON_UNKNOWN_PATH) }.from.js.resource("source/source.js") } itBehavesLike(SourceLoadBaseSpec) }) object JsSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) }.from.js.resource("source/source.js") val js = config.toJs.toText() Config { addSpec(ConfigForLoad) }.from.js.string(js) } itBehavesLike(SourceLoadBaseSpec) }) ================================================ FILE: konf-js/src/test/kotlin/com/uchuhimo/konf/source/js/JsWriterSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.js import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.Writer import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.io.ByteArrayOutputStream import java.io.StringWriter object JsWriterSpec : SubjectSpek({ subject { val config = Config { addSpec( object : ConfigSpec() { val key by optional("value") } ) } config.toJs } given("a writer") { val expectedString = """({ | key: "value" |})""".trimMargin().replace("\n", System.lineSeparator()) on("save to writer") { val writer = StringWriter() subject.toWriter(writer) it("should return a writer which contains content from config") { assertThat(writer.toString(), equalTo(expectedString)) } } on("save to output stream") { val outputStream = ByteArrayOutputStream() subject.toOutputStream(outputStream) it("should return an output stream which contains content from config") { assertThat(outputStream.toString(), equalTo(expectedString)) } } } }) ================================================ FILE: konf-js/src/test/resources/source/source.js ================================================ ({ level1: { level2: { empty: null, literalEmpty: null, present: 1, boolean: false, int: 1, short: 2, byte: 3, bigInteger: 4, long: 4, double: 1.5, float: -1.5, bigDecimal: 1.5, char: "a", string: "string", offsetTime: "10:15:30+01:00", offsetDateTime: "2007-12-03T10:15:30+01:00", zonedDateTime: "2007-12-03T10:15:30+01:00[Europe/Paris]", localDate: "2007-12-03", localTime: "10:15:30", localDateTime: "2007-12-03T10:15:30", date: "2007-12-03T10:15:30Z", year: "2007", yearMonth: "2007-12", instant: "2007-12-03T10:15:30.00Z", duration: "P2DT3H4M", simpleDuration: "200millis", size: "10k", enum: "LABEL2", array: { boolean: [ true, false ], byte: [ 1, 2, 3 ], short: [ 1, 2, 3 ], int: [ 1, 2, 3 ], long: [ 4, 5, 6 ], float: [ -1.0, 0.0, 1.0 ], double: [ -1.0, 0.0, 1.0 ], char: [ "a", "b", "c" ], object: { boolean: [ true, false ], int: [ 1, 2, 3 ], string: [ "one", "two", "three" ], enum: [ "LABEL1", "LABEL2", "LABEL3" ] } }, list: [ 1, 2, 3 ], mutableList: [ 1, 2, 3 ], listOfList: [ [ 1, 2 ], [ 3, 4 ] ], set: [ 1, 2, 1 ], sortedSet: [ 2, 1, 1, 3 ], map: { a: 1, b: 2, c: 3 }, intMap: { 1: "a", 2: "b", 3: "c" }, sortedMap: { c: 3, b: 2, a: 1 }, listOfMap: [ { a: 1, b: 2 }, { a: 3, b: 4 } ], nested: [ [ [ { a: 1 } ] ] ], pair: { first: 1, second: 2 }, clazz: { empty: null, literalEmpty: null, present: 1, boolean: false, int: 1, short: 2, byte: 3, bigInteger: 4, long: 4, double: 1.5, float: -1.5, bigDecimal: 1.5, char: "a", string: "string", offsetTime: "10:15:30+01:00", offsetDateTime: "2007-12-03T10:15:30+01:00", zonedDateTime: "2007-12-03T10:15:30+01:00[Europe/Paris]", localDate: "2007-12-03", localTime: "10:15:30", localDateTime: "2007-12-03T10:15:30", date: "2007-12-03T10:15:30Z", year: "2007", yearMonth: "2007-12", instant: "2007-12-03T10:15:30.00Z", duration: "P2DT3H4M", simpleDuration: "200millis", size: "10k", enum: "LABEL2", booleanArray: [ true, false ], nested: [ [ [ { a: 1 } ] ] ] } } } }) ================================================ FILE: konf-toml/build.gradle.kts ================================================ dependencies { api(project(":konf-core")) implementation("com.moandjiezana.toml", "toml4j", Versions.toml4j) testImplementation(testFixtures(project(":konf-core"))) } ================================================ FILE: konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.moandjiezana.toml import com.moandjiezana.toml.BooleanValueReaderWriter.BOOLEAN_VALUE_READER_WRITER import com.moandjiezana.toml.DateValueReaderWriter.DATE_VALUE_READER_WRITER import com.moandjiezana.toml.NumberValueReaderWriter.NUMBER_VALUE_READER_WRITER import com.moandjiezana.toml.StringValueReaderWriter.STRING_VALUE_READER_WRITER import java.io.IOException import java.io.StringWriter import java.io.Writer import java.util.TimeZone import java.util.regex.Pattern /** *

Converts Objects to TOML

* *

An input Object can comprise arbitrarily nested combinations of Java primitive types, * other {@link Object}s, {@link Map}s, {@link List}s, and Arrays. {@link Object}s and {@link Map}s * are output to TOML tables, and {@link List}s and Array to TOML arrays.

* *

Example usage:

*

 * class AClass {
 *   int anInt = 1;
 *   int[] anArray = { 2, 3 };
 * }
 *
 * String tomlString = new TomlWriter().write(new AClass());
 * 
*/ class Toml4jWriter { /** * Write an Object into TOML String. * * @param from the object to be written * @return a string containing the TOML representation of the given Object */ fun write(from: Any): String { try { val output = StringWriter() write(from, output) return output.toString() } catch (e: IOException) { throw RuntimeException(e) } } /** * Write an Object in TOML to a [Writer]. You MUST ensure that the [Writer]s's encoding is set to UTF-8 for the TOML to be valid. * * @param from the object to be written. Can be a Map or a custom type. Must not be null. * @param target the Writer to which TOML will be written. The Writer is not closed. * @throws IOException if target.write() fails * @throws IllegalArgumentException if from is of an invalid type */ @Throws(IOException::class) fun write(from: Any, target: Writer) { val valueWriter = Toml4jValueWriters.findWriterFor(from) if (valueWriter === NewMapValueWriter) { val context = WriterContext( IndentationPolicy(0, 0, 0), DatePolicy(TimeZone.getTimeZone("UTC"), false), target ) valueWriter.write(from, context) } else { throw IllegalArgumentException("An object of class " + from.javaClass.simpleName + " cannot produce valid TOML. Please pass in a Map or a custom type.") } } } internal object Toml4jValueWriters { fun findWriterFor(value: Any): ValueWriter { for (valueWriter in VALUE_WRITERS) { if (valueWriter.canWrite(value)) { return valueWriter } } return NewMapValueWriter } private val VALUE_WRITERS = arrayOf( STRING_VALUE_READER_WRITER, NUMBER_VALUE_READER_WRITER, BOOLEAN_VALUE_READER_WRITER, DATE_VALUE_READER_WRITER, NewMapValueWriter, NewArrayValueWriter ) } internal object NewArrayValueWriter : ArrayValueWriter() { override fun canWrite(value: Any?): Boolean = isArrayish(value) override fun write(o: Any, context: WriterContext) { val values = normalize(o) context.write('[') context.writeArrayDelimiterPadding() var first = true var firstWriter: ValueWriter? = null for (value in values) { if (first) { firstWriter = Toml4jValueWriters.findWriterFor(value!!) first = false } else { val writer = Toml4jValueWriters.findWriterFor(value!!) if (writer !== firstWriter) { throw IllegalStateException( context.contextPath + ": cannot write a heterogeneous array; first element was of type " + firstWriter + " but found " + writer ) } context.write(", ") } val writer = Toml4jValueWriters.findWriterFor(value) val isNestedOldValue = NewMapValueWriter.isNested if (writer == NewMapValueWriter) { NewMapValueWriter.isNested = true } writer.write(value, context) if (writer == NewMapValueWriter) { NewMapValueWriter.isNested = isNestedOldValue } } context.writeArrayDelimiterPadding() context.write(']') } } internal object NewMapValueWriter : ValueWriter { override fun canWrite(value: Any): Boolean { return value is Map<*, *> } var isNested: Boolean = false override fun write(value: Any, context: WriterContext) { val from = value as Map<*, *> if (hasPrimitiveValues(from)) { if (isNested) { context.indent() context.write("{\n") } else { context.writeKey() } } // Render primitive types and arrays of primitive first so they are // grouped under the same table (if there is one) for ((key, value1) in from) { val fromValue = value1 ?: continue val valueWriter = Toml4jValueWriters.findWriterFor(fromValue) if (valueWriter.isPrimitiveType()) { context.indent() context.write(quoteKey(key!!)).write(" = ") valueWriter.write(fromValue, context) if (isNested) { context.write(',') } context.write('\n') } else if (valueWriter === NewArrayValueWriter) { context.setArrayKey(key.toString()) context.write(quoteKey(key!!)).write(" = ") valueWriter.write(fromValue, context) if (isNested) { context.write(',') } context.write('\n') } } // Now render (sub)tables and arrays of tables for (key in from.keys) { val fromValue = from[key] ?: continue val valueWriter = Toml4jValueWriters.findWriterFor(fromValue) if (valueWriter === this) { valueWriter.write(fromValue, context.pushTable(quoteKey(key!!))) } } if (isNested) { context.indent() context.write("}\n") } } override fun isPrimitiveType(): Boolean { return false } private val REQUIRED_QUOTING_PATTERN = Pattern.compile("^.*[^A-Za-z\\d_-].*$") private fun quoteKey(key: Any): String { var stringKey = key.toString() val matcher = REQUIRED_QUOTING_PATTERN.matcher(stringKey) if (matcher.matches()) { stringKey = "\"" + stringKey + "\"" } return stringKey } private fun hasPrimitiveValues(values: Map<*, *>): Boolean { for (key in values.keys) { val fromValue = values[key] ?: continue val valueWriter = Toml4jValueWriters.findWriterFor(fromValue) if (valueWriter.isPrimitiveType() || valueWriter === NewArrayValueWriter) { return true } } return false } } ================================================ FILE: konf-toml/src/main/kotlin/com/uchuhimo/konf/source/DefaultTomlLoader.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.toml.TomlProvider /** * Loader for TOML source. */ val DefaultLoaders.toml get() = Loader(config, TomlProvider.orMapped()) ================================================ FILE: konf-toml/src/main/kotlin/com/uchuhimo/konf/source/DefaultTomlProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.toml.TomlProvider /** * Provider for TOML source. */ val DefaultProviders.toml get() = TomlProvider ================================================ FILE: konf-toml/src/main/kotlin/com/uchuhimo/konf/source/toml/TomlProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.toml import com.moandjiezana.toml.Toml import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.Provider import com.uchuhimo.konf.source.RegisterExtension import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.asSource import java.io.InputStream import java.io.Reader /** * Provider for TOML source. */ @RegisterExtension(["toml"]) object TomlProvider : Provider { override fun reader(reader: Reader): Source = Toml().read(reader).toMap().asSource(type = "TOML") override fun inputStream(inputStream: InputStream): Source = Toml().read(inputStream).toMap().asSource(type = "TOML") @JavaApi @JvmStatic fun get() = this } ================================================ FILE: konf-toml/src/main/kotlin/com/uchuhimo/konf/source/toml/TomlWriter.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.toml import com.moandjiezana.toml.Toml4jWriter import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.Writer import com.uchuhimo.konf.source.base.toHierarchicalMap import java.io.OutputStream /** * Writer for TOML source. */ class TomlWriter(val config: Config) : Writer { private val toml4jWriter = Toml4jWriter() override fun toWriter(writer: java.io.Writer) { writer.write(toText()) } override fun toOutputStream(outputStream: OutputStream) { outputStream.writer().use { toWriter(it) } } override fun toText(): String { return toml4jWriter.write(config.toHierarchicalMap()).replace("\n", System.lineSeparator()) } } /** * Returns writer for TOML source. */ val Config.toToml: Writer get() = TomlWriter(this) ================================================ FILE: konf-toml/src/test/kotlin/com/uchuhimo/konf/source/DefaultTomlLoaderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultTomlLoaderSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig) }.from } val item = DefaultLoadersConfig.type given("a loader") { on("load from TOML file") { val config = subject.file(tempFileOf(tomlContent, suffix = ".toml")) it("should load as auto-detected file format") { assertThat(config[item], equalTo("toml")) } } } }) ================================================ FILE: konf-toml/src/test/kotlin/com/uchuhimo/konf/source/DefaultTomlProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultTomlProviderSpec : SubjectSpek({ subject { Source.from } val item = DefaultLoadersConfig.type given("a provider") { on("provide source from TOML file") { val config = subject.file(tempFileOf(tomlContent, suffix = ".toml")).toConfig() it("should provide as auto-detected file format") { assertThat(config[item], equalTo("toml")) } } } }) ================================================ FILE: konf-toml/src/test/kotlin/com/uchuhimo/konf/source/toml/TomlProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.toml import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object TomlProviderSpec : SubjectSpek({ subject { TomlProvider } given("a TOML provider") { on("create source from reader") { val source = subject.reader("type = \"reader\"".reader()) it("should have correct type") { assertThat(source.info["type"], equalTo("TOML")) } it("should return a source which contains value from reader") { assertThat(source["type"].asValue(), equalTo("reader")) } } on("create source from input stream") { val source = subject.inputStream( tempFileOf("type = \"inputStream\"").inputStream() ) it("should have correct type") { assertThat(source.info["type"], equalTo("TOML")) } it("should return a source which contains value from input stream") { assertThat(source["type"].asValue(), equalTo("inputStream")) } } on("create source from an empty file") { val file = tempFileOf("") it("should return an empty source") { assertThat( subject.file(file).tree.children, equalTo(mutableMapOf()) ) } } } }) object TomlProviderInJavaSpec : SubjectSpek({ subject { TomlProvider.get() } itBehavesLike(TomlProviderSpec) }) ================================================ FILE: konf-toml/src/test/kotlin/com/uchuhimo/konf/source/toml/TomlSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.toml import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.ConfigForLoad import com.uchuhimo.konf.source.SourceLoadBaseSpec import com.uchuhimo.konf.source.toml import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object TomlSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) enable(Feature.FAIL_ON_UNKNOWN_PATH) }.from.toml.resource("source/source.toml") } itBehavesLike(SourceLoadBaseSpec) }) object TomlSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) }.from.toml.resource("source/source.toml") val toml = config.toToml.toText() Config { addSpec(ConfigForLoad) }.from.toml.string(toml) } itBehavesLike(SourceLoadBaseSpec) }) ================================================ FILE: konf-toml/src/test/kotlin/com/uchuhimo/konf/source/toml/TomlValueSourceSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.toml import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.sameInstance import com.natpryce.hamkrest.throws import com.uchuhimo.konf.source.ParseException import com.uchuhimo.konf.source.asSource import com.uchuhimo.konf.source.asValue import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on object TomlValueSourceSpec : Spek({ given("a TOML source") { on("get integer from long source") { it("should succeed") { assertThat(1L.asSource().asValue(), equalTo(1)) } } on("get integer from long source whose value is out of range of integer") { it("should throw ParseException") { assertThat({ Long.MAX_VALUE.asSource().asValue() }, throws()) assertThat({ Long.MIN_VALUE.asSource().asValue() }, throws()) } } on("invoke `asTomlSource`") { val source = 1.asSource() it("should return itself") { assertThat(source.asSource(), sameInstance(source)) } } } }) ================================================ FILE: konf-toml/src/test/kotlin/com/uchuhimo/konf/source/toml/TomlWriterSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.toml import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.Writer import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.io.ByteArrayOutputStream import java.io.StringWriter object TomlWriterSpec : SubjectSpek({ subject { val config = Config { addSpec( object : ConfigSpec() { val key by optional("value") } ) } config.toToml } given("a writer") { val expectedString = """key = "value" |""".trimMargin().replace("\n", System.lineSeparator()) on("save to string") { val string = subject.toText() it("should return a string which contains content from config") { assertThat(string, equalTo(expectedString)) } } on("save to writer") { val writer = StringWriter() subject.toWriter(writer) it("should return a writer which contains content from config") { assertThat(writer.toString(), equalTo(expectedString)) } } on("save to output stream") { val outputStream = ByteArrayOutputStream() subject.toOutputStream(outputStream) it("should return an output stream which contains content from config") { assertThat(outputStream.toString(), equalTo(expectedString)) } } } }) ================================================ FILE: konf-toml/src/test/resources/source/source.toml ================================================ [level1.level2] empty = "null" literalEmpty = "null" present = 1 boolean = false int = 1 short = 2 byte = 3 bigInteger = 4 long = 4 double = 1.5 float = -1.5 bigDecimal = 1.5 char = "a" string = "string" offsetTime = "10:15:30+01:00" offsetDateTime = "2007-12-03T10:15:30+01:00" zonedDateTime = "2007-12-03T10:15:30+01:00[Europe/Paris]" localDate = "2007-12-03" localTime = "10:15:30" localDateTime = "2007-12-03T10:15:30" date = 2007-12-03T10:15:30Z year = "2007" yearMonth = "2007-12" instant = 2007-12-03T10:15:30.00Z duration = "P2DT3H4M" simpleDuration = "200millis" size = "10k" enum = "LABEL2" list = [1, 2, 3] mutableList = [1, 2, 3] listOfList = [[1, 2], [3, 4]] set = [1, 2, 1] sortedSet = [2, 1, 1, 3] map = { a = 1, b = 2, c = 3 } intMap = { 1 = "a", 2 = "b", 3 = "c" } sortedMap = { c = 3, b = 2, a = 1 } nested = [[[{ a = 1 }]]] [[level1.level2.listOfMap]] a = 1 b = 2 [[level1.level2.listOfMap]] a = 3 b = 4 [level1.level2.array] boolean = [true, false] byte = [1, 2, 3] short = [1, 2, 3] int = [1, 2, 3] long = [4, 5, 6] float = [-1.0, 0.0, 1.0] double = [-1.0, 0.0, 1.0] char = ["a", "b", "c"] [level1.level2.array.object] boolean = [true, false] int = [1, 2, 3] string = ["one", "two", "three"] enum = ["LABEL1", "LABEL2", "LABEL3"] [level1.level2.pair] first = 1 second = 2 [level1.level2.clazz] empty = "null" literalEmpty = "null" present = 1 boolean = false int = 1 short = 2 byte = 3 bigInteger = 4 long = 4 double = 1.5 float = -1.5 bigDecimal = 1.5 char = "a" string = "string" offsetTime = "10:15:30+01:00" offsetDateTime = "2007-12-03T10:15:30+01:00" zonedDateTime = "2007-12-03T10:15:30+01:00[Europe/Paris]" localDate = "2007-12-03" localTime = "10:15:30" localDateTime = "2007-12-03T10:15:30" date = 2007-12-03T10:15:30Z year = "2007" yearMonth = "2007-12" instant = 2007-12-03T10:15:30.00Z duration = "P2DT3H4M" simpleDuration = "200millis" size = "10k" enum = "LABEL2" booleanArray = [true, false] nested = [[[{ a = 1 }]]] ================================================ FILE: konf-toml/src/testFixtures/kotlin/com/uchuhimo/konf/source/TomlTestUtils.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source //language=TOML const val tomlContent = """ [source.test] type = "toml" """ ================================================ FILE: konf-xml/build.gradle.kts ================================================ dependencies { api(project(":konf-core")) implementation("org.dom4j", "dom4j", Versions.dom4j) implementation("jaxen", "jaxen", Versions.jaxen) testImplementation(testFixtures(project(":konf-core"))) } ================================================ FILE: konf-xml/src/main/kotlin/com/uchuhimo/konf/source/DefaultXmlLoader.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.xml.XmlProvider /** * Loader for XML source. */ val DefaultLoaders.xml get() = Loader(config, XmlProvider.orMapped()) ================================================ FILE: konf-xml/src/main/kotlin/com/uchuhimo/konf/source/DefaultXmlProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.xml.XmlProvider /** * Provider for XML source. */ val DefaultProviders.xml get() = XmlProvider ================================================ FILE: konf-xml/src/main/kotlin/com/uchuhimo/konf/source/xml/XmlProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.xml import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.Provider import com.uchuhimo.konf.source.RegisterExtension import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.base.FlatSource import org.dom4j.Document import org.dom4j.io.SAXReader import java.io.InputStream import java.io.Reader /** * Provider for XML source. */ @RegisterExtension(["xml"]) object XmlProvider : Provider { private fun Document.toMap(): Map { val rootElement = this.rootElement val propertyNodes = rootElement.selectNodes("/configuration/property") return mutableMapOf().apply { for (property in propertyNodes) { put(property.selectSingleNode("name").text, property.selectSingleNode("value").text) } } } override fun reader(reader: Reader): Source { return FlatSource(SAXReader().read(reader).toMap(), type = "XML") } override fun inputStream(inputStream: InputStream): Source { return FlatSource(SAXReader().read(inputStream).toMap(), type = "XML") } @JavaApi @JvmStatic fun get() = this } ================================================ FILE: konf-xml/src/main/kotlin/com/uchuhimo/konf/source/xml/XmlWriter.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.xml import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.Writer import com.uchuhimo.konf.source.base.toFlatMap import org.dom4j.Document import org.dom4j.DocumentHelper import org.dom4j.io.OutputFormat import org.dom4j.io.XMLWriter import java.io.OutputStream /** * Writer for XML source. */ class XmlWriter(val config: Config) : Writer { private fun Map.toDocument(): Document { val document = DocumentHelper.createDocument() val rootElement = document.addElement("configuration") for ((key, value) in this) { val propertyElement = rootElement.addElement("property") propertyElement.addElement("name").text = key propertyElement.addElement("value").text = value } return document } private val outputFormat = OutputFormat.createPrettyPrint().apply { lineSeparator = System.lineSeparator() } override fun toWriter(writer: java.io.Writer) { val xmlWriter = XMLWriter(writer, outputFormat) xmlWriter.write(config.toFlatMap().toDocument()) xmlWriter.close() } override fun toOutputStream(outputStream: OutputStream) { val xmlWriter = XMLWriter(outputStream, outputFormat) xmlWriter.write(config.toFlatMap().toDocument()) xmlWriter.close() } } /** * Returns writer for XML source. */ val Config.toXml: Writer get() = XmlWriter(this) ================================================ FILE: konf-xml/src/test/kotlin/com/uchuhimo/konf/source/DefaultXmlLoaderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultXmlLoaderSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig) }.from } val item = DefaultLoadersConfig.type given("a loader") { on("load from XML file") { val config = subject.file(tempFileOf(xmlContent, suffix = ".xml")) it("should load as auto-detected file format") { assertThat(config[item], equalTo("xml")) } } } }) ================================================ FILE: konf-xml/src/test/kotlin/com/uchuhimo/konf/source/DefaultXmlProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultXmlProviderSpec : SubjectSpek({ subject { Source.from } val item = DefaultLoadersConfig.type given("a provider") { on("provide source from XML file") { val config = subject.file(tempFileOf(xmlContent, suffix = ".xml")).toConfig() it("should provide as auto-detected file format") { assertThat(config[item], equalTo("xml")) } } } }) ================================================ FILE: konf-xml/src/test/kotlin/com/uchuhimo/konf/source/xml/XmlProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.xml import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object XmlProviderSpec : SubjectSpek({ subject { XmlProvider } //language=XML fun xmlDoc(name: String, value: String) = """ $name $value """.trimIndent() given("a XML provider") { on("create source from reader") { val source = subject.reader(xmlDoc("type", "reader").reader()) it("should have correct type") { assertThat(source.info["type"], equalTo("XML")) } it("should return a source which contains value from reader") { assertThat(source["type"].asValue(), equalTo("reader")) } } on("create source from input stream") { val source = subject.inputStream( tempFileOf(xmlDoc("type", "inputStream")).inputStream() ) it("should have correct type") { assertThat(source.info["type"], equalTo("XML")) } it("should return a source which contains value from input stream") { assertThat(source["type"].asValue(), equalTo("inputStream")) } } on("create source from an empty file") { val file = tempFileOf("") it("should return an empty source") { assertThat( subject.file(file).tree.children, equalTo(mutableMapOf()) ) } } } }) object XmlProviderInJavaSpec : SubjectSpek({ subject { XmlProvider.get() } itBehavesLike(XmlProviderSpec) }) ================================================ FILE: konf-xml/src/test/kotlin/com/uchuhimo/konf/source/xml/XmlSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.xml import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.ConfigForLoad import com.uchuhimo.konf.source.base.FlatConfigForLoad import com.uchuhimo.konf.source.base.FlatSourceLoadBaseSpec import com.uchuhimo.konf.source.xml import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object XmlSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) }.from.xml.resource("source/source.xml") } itBehavesLike(FlatSourceLoadBaseSpec) }) object XmlSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) }.from.xml.resource("source/source.xml") val xml = config.toXml.toText() Config { addSpec(ConfigForLoad) addSpec(FlatConfigForLoad) }.from.xml.string(xml) } itBehavesLike(FlatSourceLoadBaseSpec) }) ================================================ FILE: konf-xml/src/test/kotlin/com/uchuhimo/konf/source/xml/XmlWriterSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.xml import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.Writer import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.io.ByteArrayOutputStream import java.io.StringWriter object XmlWriterSpec : SubjectSpek({ subject { val config = Config { addSpec( object : ConfigSpec() { val key by optional("value") } ) } config.toXml } given("a writer") { val expectedString = """ | | | | | key | value | | |""".trimMargin().replace("\n", System.lineSeparator()) on("save to writer") { val writer = StringWriter() subject.toWriter(writer) it("should return a writer which contains content from config") { assertThat(writer.toString(), equalTo(expectedString)) } } on("save to output stream") { val outputStream = ByteArrayOutputStream() subject.toOutputStream(outputStream) it("should return an output stream which contains content from config") { assertThat(outputStream.toString(), equalTo(expectedString)) } } } }) ================================================ FILE: konf-xml/src/test/resources/source/source.xml ================================================ level1.level2.empty null level1.level2.literalEmpty null level1.level2.present 1 level1.level2.boolean false level1.level2.int 1 level1.level2.short 2 level1.level2.byte 3 level1.level2.bigInteger 4 level1.level2.long 4 level1.level2.double 1.5 level1.level2.float -1.5 level1.level2.bigDecimal 1.5 level1.level2.char a level1.level2.string string level1.level2.offsetTime 10:15:30+01:00 level1.level2.offsetDateTime 2007-12-03T10:15:30+01:00 level1.level2.zonedDateTime 2007-12-03T10:15:30+01:00[Europe/Paris] level1.level2.localDate 2007-12-03 level1.level2.localTime 10:15:30 level1.level2.localDateTime 2007-12-03T10:15:30 level1.level2.date 2007-12-03T10:15:30Z level1.level2.year 2007 level1.level2.yearMonth 2007-12 level1.level2.instant 2007-12-03T10:15:30.00Z level1.level2.duration P2DT3H4M level1.level2.simpleDuration 200millis level1.level2.size 10k level1.level2.enum LABEL2 level1.level2.list 1,2,3 level1.level2.mutableList 1,2,3 level1.level2.listOfList.0 1,2 level1.level2.listOfList.1 3,4 level1.level2.set 1,2,1 level1.level2.sortedSet 2,1,1,3 level1.level2.map.a 1 level1.level2.map.b 2 level1.level2.map.c 3 level1.level2.intMap.1 a level1.level2.intMap.2 b level1.level2.intMap.3 c level1.level2.sortedMap.c 3 level1.level2.sortedMap.b 2 level1.level2.sortedMap.a 1 level1.level2.nested.0.0.0.a 1 level1.level2.listOfMap.0.a 1 level1.level2.listOfMap.0.b 2 level1.level2.listOfMap.1.a 3 level1.level2.listOfMap.1.b 4 level1.level2.array.boolean true,false level1.level2.array.byte 1,2,3 level1.level2.array.short 1,2,3 level1.level2.array.int 1,2,3 level1.level2.array.long 4,5,6 level1.level2.array.float -1, 0.0, 1 level1.level2.array.double -1, 0.0, 1 level1.level2.array.char a,b,c level1.level2.array.object.boolean true,false level1.level2.array.object.int 1,2,3 level1.level2.array.object.string one,two,three level1.level2.array.object.enum LABEL1,LABEL2,LABEL3 level1.level2.pair.first 1 level1.level2.pair.second 2 level1.level2.clazz.empty null level1.level2.clazz.literalEmpty null level1.level2.clazz.present 1 level1.level2.clazz.boolean false level1.level2.clazz.int 1 level1.level2.clazz.short 2 level1.level2.clazz.byte 3 level1.level2.clazz.bigInteger 4 level1.level2.clazz.long 4 level1.level2.clazz.double 1.5 level1.level2.clazz.float -1.5 level1.level2.clazz.bigDecimal 1.5 level1.level2.clazz.char a level1.level2.clazz.string string level1.level2.clazz.offsetTime 10:15:30+01:00 level1.level2.clazz.offsetDateTime 2007-12-03T10:15:30+01:00 level1.level2.clazz.zonedDateTime 2007-12-03T10:15:30+01:00[Europe/Paris] level1.level2.clazz.localDate 2007-12-03 level1.level2.clazz.localTime 10:15:30 level1.level2.clazz.localDateTime 2007-12-03T10:15:30 level1.level2.clazz.date 2007-12-03T10:15:30Z level1.level2.clazz.year 2007 level1.level2.clazz.yearMonth 2007-12 level1.level2.clazz.instant 2007-12-03T10:15:30.00Z level1.level2.clazz.duration P2DT3H4M level1.level2.clazz.simpleDuration 200millis level1.level2.clazz.size 10k level1.level2.clazz.enum LABEL2 level1.level2.clazz.booleanArray true,false level1.level2.clazz.nested.0.0.0.a 1 level1.level2.flatClass.stringWithComma string,with,comma level1.level2.emptyList level1.level2.emptySet level1.level2.emptyArray level1.level2.emptyObjectArray level1.level2.singleElementList 1 level1.level2.multipleElementsList 1,2 level1.level2.flatClass.stringWithComma string,with,comma level1.level2.flatClass.emptyList level1.level2.flatClass.emptySet level1.level2.flatClass.emptyArray level1.level2.flatClass.emptyObjectArray level1.level2.flatClass.singleElementList 1 level1.level2.flatClass.multipleElementsList 1,2 ================================================ FILE: konf-xml/src/testFixtures/kotlin/com/uchuhimo/konf/source/XmlTestUtils.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source //language=XML val xmlContent = """ source.test.type xml """.trim() ================================================ FILE: konf-yaml/build.gradle.kts ================================================ dependencies { api(project(":konf-core")) implementation("org.yaml", "snakeyaml", Versions.yaml) testImplementation(testFixtures(project(":konf-core"))) } ================================================ FILE: konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/DefaultYamlLoader.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.yaml.YamlProvider /** * Loader for YAML source. */ val DefaultLoaders.yaml get() = Loader(config, YamlProvider.orMapped()) ================================================ FILE: konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/DefaultYamlProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.uchuhimo.konf.source.yaml.YamlProvider /** * Provider for YAML source. */ val DefaultProviders.yaml get() = YamlProvider ================================================ FILE: konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlProvider.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.yaml import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.Provider import com.uchuhimo.konf.source.RegisterExtension import com.uchuhimo.konf.source.Source import com.uchuhimo.konf.source.asSource import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.constructor.AbstractConstruct import org.yaml.snakeyaml.constructor.SafeConstructor import org.yaml.snakeyaml.nodes.Node import org.yaml.snakeyaml.nodes.ScalarNode import org.yaml.snakeyaml.nodes.Tag import java.io.InputStream import java.io.Reader /** * Provider for YAML source. */ @RegisterExtension(["yml", "yaml"]) object YamlProvider : Provider { override fun reader(reader: Reader): Source { val yaml = Yaml(YamlConstructor()) val value = yaml.load(reader) if (value == "null") { return mapOf().asSource("YAML") } else { return value.asSource("YAML") } } override fun inputStream(inputStream: InputStream): Source { val yaml = Yaml(YamlConstructor()) val value = yaml.load(inputStream) if (value == "null") { return mapOf().asSource("YAML") } else { return value.asSource("YAML") } } @JavaApi @JvmStatic fun get() = this } private class YamlConstructor : SafeConstructor() { init { yamlConstructors[Tag.NULL] = object : AbstractConstruct() { override fun construct(node: Node?): Any? { if (node != null) { constructScalar(node as ScalarNode) } return "null" } } } } ================================================ FILE: konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlWriter.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.yaml import com.uchuhimo.konf.Config import com.uchuhimo.konf.source.Writer import com.uchuhimo.konf.source.base.toHierarchicalMap import org.yaml.snakeyaml.DumperOptions import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.constructor.SafeConstructor import org.yaml.snakeyaml.representer.Representer import java.io.OutputStream /** * Writer for YAML source. */ class YamlWriter(val config: Config) : Writer { private val yaml = Yaml( SafeConstructor(), Representer(), DumperOptions().apply { defaultFlowStyle = DumperOptions.FlowStyle.BLOCK lineBreak = DumperOptions.LineBreak.getPlatformLineBreak() } ) override fun toWriter(writer: java.io.Writer) { yaml.dump(config.toHierarchicalMap(), writer) } override fun toOutputStream(outputStream: OutputStream) { outputStream.writer().use { toWriter(it) } } } /** * Returns writer for YAML source. */ val Config.toYaml: Writer get() = YamlWriter(this) ================================================ FILE: konf-yaml/src/test/kotlin/com/uchuhimo/konf/source/DefaultYamlLoaderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultYamlLoaderSpec : SubjectSpek({ subject { Config { addSpec(DefaultLoadersConfig) }.from } val item = DefaultLoadersConfig.type given("a loader") { on("load from YAML file") { val config = subject.file(tempFileOf(yamlContent, suffix = ".yaml")) it("should load as auto-detected file format") { assertThat(config[item], equalTo("yaml")) } } } }) ================================================ FILE: konf-yaml/src/test/kotlin/com/uchuhimo/konf/source/DefaultYamlProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek object DefaultYamlProviderSpec : SubjectSpek({ subject { Source.from } val item = DefaultLoadersConfig.type given("a provider") { on("provide source from YAML file") { val config = subject.file(tempFileOf(yamlContent, suffix = ".yaml")).toConfig() it("should provide as auto-detected file format") { assertThat(config[item], equalTo("yaml")) } } } }) ================================================ FILE: konf-yaml/src/test/kotlin/com/uchuhimo/konf/source/yaml/YamlProviderSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.yaml import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.source.asValue import com.uchuhimo.konf.tempFileOf import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike object YamlProviderSpec : SubjectSpek({ subject { YamlProvider } given("a YAML provider") { on("create source from reader") { val source = subject.reader("type: reader".reader()) it("should have correct type") { assertThat(source.info["type"], equalTo("YAML")) } it("should return a source which contains value from reader") { assertThat(source["type"].asValue(), equalTo("reader")) } } on("create source from input stream") { val source = subject.inputStream( tempFileOf("type: inputStream").inputStream() ) it("should have correct type") { assertThat(source.info["type"], equalTo("YAML")) } it("should return a source which contains value from input stream") { assertThat(source["type"].asValue(), equalTo("inputStream")) } } on("create source from an empty file") { val file = tempFileOf("") it("should return an empty source") { assertThat( subject.file(file).tree.children, equalTo(mutableMapOf()) ) } } } }) object YamlProviderInJavaSpec : SubjectSpek({ subject { YamlProvider.get() } itBehavesLike(YamlProviderSpec) }) ================================================ FILE: konf-yaml/src/test/kotlin/com/uchuhimo/konf/source/yaml/YamlSourceLoadSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.yaml import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.ConfigForLoad import com.uchuhimo.konf.source.SourceLoadBaseSpec import com.uchuhimo.konf.source.yaml import com.uchuhimo.konf.toValue import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import org.jetbrains.spek.subject.itBehavesLike import kotlin.test.assertTrue object YamlSourceLoadSpec : SubjectSpek({ subject { Config { addSpec(ConfigForLoad) enable(Feature.FAIL_ON_UNKNOWN_PATH) }.from.yaml.resource("source/source.yaml") } itBehavesLike(SourceLoadBaseSpec) given("a config") { on("load a YAML with an int key") { val config = Config().from.yaml.string( """ tree: 1: myVal: true """.trimIndent() ) it("should treat it as a string key") { assertTrue { config.at("tree.1.myVal").toValue() } } } on("load a YAML with a long key") { val config = Config().from.yaml.string( """ tree: 2147483648: myVal: true """.trimIndent() ) it("should treat it as a string key") { assertTrue { config.at("tree.2147483648.myVal").toValue() } } } on("load a YAML with a BigInteger key") { val config = Config().from.yaml.string( """ tree: 9223372036854775808: myVal: true """.trimIndent() ) it("should treat it as a string key") { assertTrue { config.at("tree.9223372036854775808.myVal").toValue() } } } on("load a YAML with a top-level list") { val config = Config().from.yaml.string( """ - a - b """.trimIndent() ) it("should treat it as a list") { assertThat(config.toValue(), equalTo(listOf("a", "b"))) } } } }) object YamlSourceReloadSpec : SubjectSpek({ subject { val config = Config { addSpec(ConfigForLoad) }.from.yaml.resource("source/source.yaml") val yaml = config.toYaml.toText() Config { addSpec(ConfigForLoad) }.from.yaml.string(yaml) } itBehavesLike(SourceLoadBaseSpec) }) ================================================ FILE: konf-yaml/src/test/kotlin/com/uchuhimo/konf/source/yaml/YamlWriterSpec.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source.yaml import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.Writer import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on import org.jetbrains.spek.subject.SubjectSpek import java.io.ByteArrayOutputStream import java.io.StringWriter object YamlWriterSpec : SubjectSpek({ subject { val config = Config { addSpec( object : ConfigSpec() { val key by optional("value") } ) } config.toYaml } given("a writer") { val expectedString = "key: value" + System.lineSeparator() on("save to writer") { val writer = StringWriter() subject.toWriter(writer) it("should return a writer which contains content from config") { assertThat(writer.toString(), equalTo(expectedString)) } } on("save to output stream") { val outputStream = ByteArrayOutputStream() subject.toOutputStream(outputStream) it("should return an output stream which contains content from config") { assertThat(outputStream.toString(), equalTo(expectedString)) } } } }) ================================================ FILE: konf-yaml/src/test/resources/source/source.yaml ================================================ level1: level2: empty: "null" literalEmpty: null present: 1 boolean: false int: 1 short: 2 byte: 3 bigInteger: 4 long: 4 double: 1.5 float: -1.5 bigDecimal: 1.5 char: "a" string: string offsetTime: 10:15:30+01:00 offsetDateTime: "2007-12-03T10:15:30+01:00" zonedDateTime: 2007-12-03T10:15:30+01:00[Europe/Paris] localDate: 2007-12-03 localTime: "10:15:30" localDateTime: 2007-12-03T10:15:30 date: 2007-12-03T10:15:30Z year: "2007" yearMonth: 2007-12 instant: 2007-12-03T10:15:30.00Z duration: P2DT3H4M simpleDuration: 200millis size: 10k enum: LABEL2 array: boolean: - true - false byte: [1, 2, 3] short: [1, 2, 3] int: [1, 2, 3] long: [4, 5, 6] float: [-1, 0.0, 1] double: [-1, 0.0, 1] char: [a, b, c] object: boolean: [true, false] int: [1, 2, 3] string: [one, two, three] enum: [LABEL1, LABEL2, LABEL3] list: [1, 2, 3] mutableList: [1, 2, 3] listOfList: - - 1 - 2 - [3, 4] set: [1, 2, 1] sortedSet: [2, 1, 1, 3] map: a: 1 b: 2 c: 3 intMap: 1: a 2: b 3: c sortedMap: { c: 3, b: 2, a: 1 } listOfMap: - a: 1 b: 2 - a: 3 b: 4 nested: [[[{ a: 1 }]]] pair: first: 1 second: 2 clazz: empty: "null" literalEmpty: null present: 1 boolean: false int: 1 short: 2 byte: 3 bigInteger: 4 long: 4 double: 1.5 float: -1.5 bigDecimal: 1.5 char: "a" string: string offsetTime: 10:15:30+01:00 offsetDateTime: "2007-12-03T10:15:30+01:00" zonedDateTime: 2007-12-03T10:15:30+01:00[Europe/Paris] localDate: 2007-12-03 localTime: "10:15:30" localDateTime: 2007-12-03T10:15:30 date: 2007-12-03T10:15:30Z year: "2007" yearMonth: 2007-12 instant: 2007-12-03T10:15:30.00Z duration: P2DT3H4M simpleDuration: 200millis size: 10k enum: LABEL2 booleanArray: - true - false nested: [[[{ a: 1 }]]] ================================================ FILE: konf-yaml/src/testFixtures/kotlin/com/uchuhimo/konf/source/YamlTestUtils.kt ================================================ /* * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uchuhimo.konf.source //language=YAML const val yamlContent = """ source: test: type: yaml """ ================================================ FILE: settings.gradle.kts ================================================ pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } rootProject.name = "konf" include( "konf-core", "konf-git", "konf-hocon", "konf-js", "konf-toml", "konf-xml", "konf-yaml", "konf-all" ) plugins { id("com.gradle.enterprise") version "3.0" } gradleEnterprise { buildScan { termsOfServiceUrl = "https://gradle.com/terms-of-service" termsOfServiceAgree = "yes" } }