├── .circleci ├── config.pkl └── config.yml ├── .gitignore ├── CODE_OF_CONDUCT.adoc ├── CONTRIBUTING.adoc ├── LICENSE.txt ├── MAINTAINERS.adoc ├── NOTICE.txt ├── README.adoc ├── SECURITY.adoc ├── build.gradle.kts ├── buildSrc ├── settings.gradle.kts └── src │ └── main │ └── resources │ ├── license-header.line-comment.txt │ └── license-header.star-block.txt ├── docs ├── antora.yml ├── modules │ └── ROOT │ │ └── pages │ │ ├── changelog.adoc │ │ ├── index.adoc │ │ ├── installation.adoc │ │ └── usage.adoc └── nav.adoc ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── samples ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── spring-boot-external-config │ ├── README.adoc │ ├── build.gradle.kts │ ├── config │ │ ├── AppConfig.pkl │ │ └── application.pkl │ └── src │ │ └── main │ │ ├── java │ │ └── samples │ │ │ └── boot │ │ │ ├── Application.java │ │ │ └── Server.java │ │ └── resources │ │ └── application.properties ├── spring-boot-kotlin │ ├── README.adoc │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── kotlin │ │ └── samples │ │ │ └── kotlin │ │ │ ├── Application.kt │ │ │ └── Server.kt │ │ └── resources │ │ ├── AppConfig.pkl │ │ └── application.pkl └── spring-boot │ ├── README.adoc │ ├── build.gradle.kts │ └── src │ └── main │ ├── java │ └── samples │ │ └── boot │ │ ├── Application.java │ │ └── Server.java │ └── resources │ ├── AppConfig.pkl │ └── application.pkl ├── settings.gradle.kts └── src ├── main ├── java │ └── org │ │ └── pkl │ │ └── spring │ │ └── boot │ │ ├── PklAutoConfiguration.java │ │ ├── PklPropertySourceLoader.java │ │ └── package-info.java └── resources │ └── META-INF │ └── spring.factories └── test ├── java └── org │ └── pkl │ └── spring │ └── boot │ ├── ConfigTest.java │ └── ConfigTestApp.java └── resources └── application.pkl /.circleci/config.pkl: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | //===----------------------------------------------------------------------===// 16 | amends "package://pkg.pkl-lang.org/pkl-project-commons/pkl.impl.circleci@1.1.1#/PklCI.pkl" 17 | 18 | jobs { 19 | default { 20 | docker { 21 | new { image = "cimg/openjdk:17.0" } 22 | } 23 | } 24 | ["build"] { 25 | steps { 26 | "checkout" 27 | new RunStep { 28 | name = "Building" 29 | command = "./gradlew build" 30 | } 31 | } 32 | } 33 | ["deploy-release"] { 34 | steps { 35 | "checkout" 36 | new RunStep { 37 | command = """ 38 | ./gradlew -DreleaseBuild=true publishToSonatype closeAndReleaseSonatypeStagingRepository 39 | """ 40 | } 41 | } 42 | } 43 | ["deploy-snapshot"] { 44 | steps { 45 | "checkout" 46 | new RunStep { 47 | command = "./gradlew build publishToSonatype" 48 | } 49 | } 50 | } 51 | ["github-release"] { 52 | docker = new { 53 | new { image = "maniator/gh:v2.40.1" } 54 | } 55 | steps { 56 | new RunStep { 57 | command = #""" 58 | gh release create "${CIRCLE_TAG}" \ 59 | --title "${CIRCLE_TAG}" \ 60 | --target "${CIRCLE_SHA1}" \ 61 | --verify-tag \ 62 | --notes "Release notes: https://pkl-lang.org/spring/current/changelog.html#release-${CIRCLE_TAG}" \ 63 | --repo "${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" 64 | """# 65 | } 66 | } 67 | } 68 | } 69 | 70 | triggerDocsBuild = "release" 71 | 72 | prb { 73 | jobs { 74 | "build" 75 | } 76 | } 77 | 78 | main { 79 | jobs { 80 | new { 81 | ["deploy-snapshot"] { 82 | context { 83 | "pkl-maven-release" 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | release { 91 | jobs { 92 | new { 93 | ["github-release"] { 94 | context { 95 | "pkl-github-release" 96 | } 97 | } 98 | } 99 | new { 100 | ["deploy-release"] { 101 | context { 102 | "pkl-maven-release" 103 | } 104 | requires { 105 | "github-release" 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Generated from CircleCI.pkl. DO NOT EDIT. 2 | version: '2.1' 3 | orbs: 4 | pr-approval: apple/pr-approval@0.1.0 5 | jobs: 6 | build: 7 | steps: 8 | - checkout 9 | - run: 10 | command: ./gradlew build 11 | name: Building 12 | docker: 13 | - image: cimg/openjdk:17.0 14 | deploy-release: 15 | steps: 16 | - checkout 17 | - run: 18 | command: ./gradlew -DreleaseBuild=true publishToSonatype closeAndReleaseSonatypeStagingRepository 19 | docker: 20 | - image: cimg/openjdk:17.0 21 | deploy-snapshot: 22 | steps: 23 | - checkout 24 | - run: 25 | command: ./gradlew build publishToSonatype 26 | docker: 27 | - image: cimg/openjdk:17.0 28 | github-release: 29 | steps: 30 | - run: 31 | command: |- 32 | gh release create "${CIRCLE_TAG}" \ 33 | --title "${CIRCLE_TAG}" \ 34 | --target "${CIRCLE_SHA1}" \ 35 | --verify-tag \ 36 | --notes "Release notes: https://pkl-lang.org/spring/current/changelog.html#release-${CIRCLE_TAG}" \ 37 | --repo "${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" 38 | docker: 39 | - image: maniator/gh:v2.40.1 40 | trigger-docsite-build: 41 | steps: 42 | - run: 43 | command: |- 44 | curl --location \ 45 | --request POST \ 46 | --header "Content-Type: application/json" \ 47 | -u "${CIRCLE_TOKEN}:" \ 48 | --data '{ "branch": "main" }' \ 49 | "https://circleci.com/api/v2/project/github/apple/pkl-lang.org/pipeline" 50 | name: Triggering docsite build 51 | docker: 52 | - image: cimg/openjdk:17.0 53 | - image: cimg/base:current 54 | workflows: 55 | prb: 56 | jobs: 57 | - hold: 58 | type: approval 59 | - pr-approval/authenticate: 60 | context: pkl-pr-approval 61 | - build: 62 | requires: 63 | - hold 64 | when: 65 | matches: 66 | value: << pipeline.git.branch >> 67 | pattern: ^pull/\d+(/head)?$ 68 | main: 69 | jobs: 70 | - deploy-snapshot: 71 | context: 72 | - pkl-maven-release 73 | when: 74 | equal: 75 | - main 76 | - << pipeline.git.branch >> 77 | release: 78 | jobs: 79 | - github-release: 80 | context: 81 | - pkl-github-release 82 | filters: 83 | branches: 84 | ignore: /.*/ 85 | tags: 86 | only: /^v?\d+\.\d+\.\d+$/ 87 | - deploy-release: 88 | requires: 89 | - github-release 90 | context: 91 | - pkl-maven-release 92 | filters: 93 | branches: 94 | ignore: /.*/ 95 | tags: 96 | only: /^v?\d+\.\d+\.\d+$/ 97 | - trigger-docsite-build: 98 | requires: 99 | - deploy-release 100 | context: 101 | - pkl-pr-approval 102 | filters: 103 | branches: 104 | ignore: /.*/ 105 | tags: 106 | only: /^v?\d+\.\d+\.\d+$/ 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # mac 2 | .DS_STORE 3 | 4 | # gradle 5 | .gradle/ 6 | build/ 7 | generated 8 | 9 | # intellij 10 | .idea/ 11 | !.idea/codestyles/ 12 | !.idea/inspectionProfiles/ 13 | !.idea/vcs.xml 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.adoc: -------------------------------------------------------------------------------- 1 | == Code of Conduct 2 | 3 | === Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our 7 | project and our community a harassment-free experience for everyone, 8 | regardless of age, body size, disability, ethnicity, sex 9 | characteristics, gender identity and expression, level of experience, 10 | education, socio-economic status, nationality, personal appearance, 11 | race, religion, or sexual identity and orientation. 12 | 13 | === Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual 27 | attention or advances 28 | * Trolling, insulting/derogatory comments, and personal or political 29 | attacks 30 | * Public or private harassment 31 | * Publishing others’ private information, such as a physical or 32 | electronic address, without explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a 34 | professional setting 35 | 36 | === Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of 39 | acceptable behavior and are expected to take appropriate and fair 40 | corrective action in response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, 43 | or reject comments, commits, code, wiki edits, issues, and other 44 | contributions that are not aligned to this Code of Conduct, or to ban 45 | temporarily or permanently any contributor for other behaviors that they 46 | deem inappropriate, threatening, offensive, or harmful. 47 | 48 | === Scope 49 | 50 | This Code of Conduct applies within all project spaces, and it also 51 | applies when an individual is representing the project or its community 52 | in public spaces. Examples of representing a project or community 53 | include using an official project e-mail address, posting via an 54 | official social media account, or acting as an appointed representative 55 | at an online or offline event. Representation of a project may be 56 | further defined and clarified by project maintainers. 57 | 58 | === Enforcement 59 | 60 | Instances of abusive, harassing, or otherwise unacceptable behavior may 61 | be reported by contacting the open source team at 62 | opensource-conduct@group.apple.com. All complaints will be reviewed and 63 | investigated and will result in a response that is deemed necessary and 64 | appropriate to the circumstances. The project team is obligated to 65 | maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted 67 | separately. 68 | 69 | Project maintainers who do not follow or enforce the Code of Conduct in 70 | good faith may face temporary or permanent repercussions as determined 71 | by other members of the project’s leadership. 72 | 73 | === Attribution 74 | 75 | This Code of Conduct is adapted from the 76 | https://www.contributor-covenant.org[Contributor Covenant], version 1.4, 77 | available at 78 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | :uri-github-issue-pkl-spring: https://github.com/apple/pkl-spring/issues/new 2 | :uri-seven-rules: https://cbea.ms/git-commit/#seven-rules 3 | 4 | = Pkl Spring Contributors Guide 5 | 6 | Welcome to the Pkl community, and thank you for contributing! 7 | This guide explains how to get involved. 8 | 9 | * <> 10 | * <> 11 | * <> 12 | 13 | == Licensing 14 | 15 | Pkl Spring is released under the Apache 2.0 license. 16 | This is why we require that, by submitting a pull request, you acknowledge that you have the right to license your contribution to Apple and the community, and agree that your contribution is licensed under the Apache 2.0 license. 17 | 18 | == Issue Tracking 19 | 20 | To file a bug or feature request, use {uri-github-issue-pkl-spring}[GitHub]. 21 | Be sure to include the following information: 22 | 23 | * Context 24 | ** What are/were you trying to achieve? 25 | ** What's the impact of this bug/feature? 26 | 27 | == Pull Requests 28 | 29 | When preparing a pull request, follow this checklist: 30 | 31 | * Imitate the conventions of surrounding code. 32 | * Follow the {uri-seven-rules}[seven rules] of great Git commit messages: 33 | ** Separate subject from body with a blank line. 34 | ** Limit the subject line to 50 characters. 35 | ** Capitalize the subject line. 36 | ** Do not end the subject line with a period. 37 | ** Use the imperative mood in the subject line. 38 | ** Wrap the body at 72 characters. 39 | ** Use the body to explain what and why vs. how. 40 | 41 | == Maintainers 42 | 43 | The project’s maintainers (those with write access to the upstream repository) are listed in link:MAINTAINERS.adoc[]. 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MAINTAINERS.adoc: -------------------------------------------------------------------------------- 1 | = MAINTAINERS 2 | 3 | This page lists all active Maintainers of this repository. 4 | 5 | See link:CONTRIBUTING.adoc[] for general contribution guidelines. 6 | 7 | == Maintainers (in alphabetical order) 8 | 9 | * https://github.com/bioball[Daniel Chao] 10 | * https://github.com/stackoverflow[Islon Scherer] 11 | * https://github.com/holzensp[Philip Hölzenspies] 12 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2024-2025 Apple Inc. and the Pkl project authors 2 | 3 | Portions of this software includes code from "Gradle" by Gradle, Inc. 4 | 5 | Copyright 2015 the original author or authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | https://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Pkl Spring Boot Integration image:https://circleci.com/gh/apple/pkl-spring.svg?style=svg["pkl-spring", link="https://circleci.com/gh/apple/pkl-spring"] 2 | :uri-docs: https://pkl-lang.org/spring/current 3 | :uri-installation: {uri-docs}/installation 4 | :uri-usage: {uri-docs}/usage 5 | 6 | _pkl-spring_ is a Spring Boot extension for configuring Boot apps with Pkl. 7 | 8 | Because _pkl-spring_ plugs into Spring Boot's standard configuration mechanism, 9 | configuring your Boot apps with Pkl works much the same as configuring them with Java properties or YAML. 10 | 11 | Continue with {uri-installation}[installation] and {uri-usage}[usage instructions]. 12 | 13 | == Development 14 | 15 | === Setup 16 | 17 | * Install JDK 17 18 | * Install the latest IntelliJ IDEA 19 | ** To import the project into IntelliJ, go to File->Open and select the project's root directory. 20 | If the project is opened but not imported, look for a popup in the lower right corner 21 | and click its "Import Gradle Project" link. 22 | 23 | === Common Build Commands 24 | 25 | [source,shell] 26 | ---- 27 | ./gradlew test 28 | ./gradlew build 29 | cd samples && ./gradlew build 30 | ---- 31 | -------------------------------------------------------------------------------- /SECURITY.adoc: -------------------------------------------------------------------------------- 1 | = Security 2 | 3 | For the protection of our community, the Pkl team does not disclose, discuss, or confirm security issues until our investigation is complete and any necessary updates are generally available. 4 | 5 | == Reporting a security vulnerability 6 | 7 | If you have discovered a security vulnerability within the Pkl Spring project, please report it to us. 8 | We welcome reports from everyone, including security researchers, developers, and users. 9 | 10 | Security vulnerabilities may be reported on the link:https://security.apple.com/submit[Report a vulnerability] form. 11 | When submitting a vulnerability, select "Apple Devices and Software" as the affected platform, and "Open Source" as the affected area. 12 | 13 | For more information, see https://pkl-lang.org/security.html. 14 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.nio.charset.StandardCharsets 2 | import java.util.* 3 | 4 | @Suppress("DSL_SCOPE_VIOLATION") 5 | plugins { 6 | `java-library` 7 | `maven-publish` 8 | alias(libs.plugins.pkl) 9 | alias(libs.plugins.nexusPublish) 10 | alias(libs.plugins.spotless) 11 | signing 12 | } 13 | 14 | private val isReleaseBuild = System.getProperty("releaseBuild") != null 15 | 16 | version = if (isReleaseBuild) version else "$version-SNAPSHOT" 17 | 18 | val originalRemoteName = System.getenv("PKL_ORIGINAL_REMOTE_NAME") ?: "origin" 19 | 20 | spotless { 21 | ratchetFrom = "$originalRemoteName/main" 22 | 23 | format("pkl") { 24 | target("*.pkl") 25 | licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.line-comment.txt"), "/// ") 26 | } 27 | java { 28 | googleJavaFormat(libs.versions.googleJavaFormat.get()) 29 | target("src/*/java/**/*.java") 30 | target("samples/*/src/*/java/**/*.java") 31 | licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt")) 32 | } 33 | kotlin { 34 | ktfmt(libs.versions.ktfmt.get()).googleStyle() 35 | target("samples/*/src/*/kotlin/**/*.kt") 36 | licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt")) 37 | } 38 | } 39 | 40 | nexusPublishing { 41 | repositories { 42 | sonatype { 43 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 44 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 45 | } 46 | } 47 | } 48 | 49 | java { 50 | sourceCompatibility = JavaVersion.VERSION_17 51 | } 52 | 53 | dependencies { 54 | compileOnly(libs.springBoot) 55 | compileOnly(libs.spotbugsAnnotations) 56 | 57 | // `api` instead of `implementation` 58 | // so that users of pkl-spring don't need to add pkl-config-java-all 59 | // to be able to compile their generated config classes 60 | api(libs.pklConfigJavaAll) 61 | 62 | testImplementation(libs.springTest) 63 | testImplementation(libs.springBoot) 64 | testImplementation(libs.springBootTest) 65 | testImplementation(libs.springBootAutoConfigure) 66 | testImplementation(libs.junitApi) 67 | testImplementation(libs.junitParams) 68 | testImplementation(libs.assertJ) 69 | 70 | testRuntimeOnly(libs.junitEngine) 71 | } 72 | 73 | pkl { 74 | javaCodeGenerators { 75 | register("configClasses") { 76 | generateGetters.set(true) 77 | generateSpringBootConfig.set(true) 78 | sourceSet.set(sourceSets.test.get()) 79 | sourceModules.set(files("src/test/resources/application.pkl")) 80 | } 81 | } 82 | } 83 | 84 | tasks.compileTestJava { 85 | // required by Boot 3.x (usually handled by Boot's Gradle plugin) 86 | options.compilerArgs = options.compilerArgs + "-parameters" 87 | } 88 | 89 | tasks.jar { 90 | manifest { 91 | attributes("Automatic-Module-Name" to "org.pkl.spring") 92 | } 93 | } 94 | 95 | tasks.test { 96 | useJUnitPlatform() 97 | reports { 98 | html.required.set(true) 99 | } 100 | } 101 | 102 | tasks.javadoc { 103 | classpath = sourceSets.main.get().output + sourceSets.main.get().compileClasspath 104 | source = sourceSets.main.get().allJava 105 | title = "${project.name} ${project.version} API" 106 | } 107 | 108 | val sourcesJar by tasks.registering(Jar::class) { 109 | archiveClassifier.set("sources") 110 | from(sourceSets.main.get().allSource) 111 | } 112 | 113 | val javadocJar by tasks.registering(Jar::class) { 114 | archiveClassifier.set("javadoc") 115 | from(tasks.javadoc) 116 | } 117 | 118 | publishing { 119 | publications { 120 | create("library") { 121 | from(components["java"]) 122 | artifact(sourcesJar) 123 | artifact(javadocJar) 124 | pom { 125 | name.set("pkl-spring") 126 | url.set("https://github.com/apple/pkl-spring") 127 | description.set("Spring Boot extension for configuring Boot apps with Pkl.") 128 | licenses { 129 | license { 130 | name = "Apache 2.0" 131 | url = "https://github.com/apple/pkl-spring/blob/main/LICENSE.txt" 132 | } 133 | } 134 | developers { 135 | developer { 136 | id.set("pkl-authors") 137 | name.set("The Pkl Authors") 138 | email.set("pkl-oss@group.apple.com") 139 | } 140 | } 141 | scm { 142 | connection.set("scm:git:git://github.com/apple/pkl.git") 143 | developerConnection.set("scm:git:ssh://github.com/apple/pkl.git") 144 | url.set("https://github.com/apple/pkl/tree/${if (isReleaseBuild) version else "main"}") 145 | } 146 | issueManagement { 147 | system.set("GitHub Issues") 148 | url.set("https://github.com/apple/pkl-spring/issues") 149 | } 150 | ciManagement { 151 | system.set("Circle CI") 152 | url.set("https://app.circleci.com/pipelines/github/apple/pkl-spring") 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | val printVersion by tasks.registering { 160 | doFirst { println(version) } 161 | } 162 | 163 | signing { 164 | // provided as env vars `ORG_GRADLE_PROJECT_signingKey` and `ORG_GRADLE_PROJECT_signingPassword` 165 | // in CI. 166 | val signingKey = (findProperty("signingKey") as String?) 167 | ?.let { Base64.getDecoder().decode(it).toString(StandardCharsets.US_ASCII) } 168 | val signingPassword = findProperty("signingPassword") as String? 169 | if (signingKey != null && signingPassword != null) { 170 | useInMemoryPgpKeys(signingKey, signingPassword) 171 | } 172 | sign(publishing.publications["library"]) 173 | } 174 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "buildSrc" 2 | -------------------------------------------------------------------------------- /buildSrc/src/main/resources/license-header.line-comment.txt: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright © $YEAR Apple Inc. and the Pkl project authors. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | //===----------------------------------------------------------------------===// 16 | 17 | -------------------------------------------------------------------------------- /buildSrc/src/main/resources/license-header.star-block.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © $YEAR Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /docs/antora.yml: -------------------------------------------------------------------------------- 1 | name: spring 2 | title: Spring Boot Integration 3 | version: 0.18.0 4 | nav: 5 | - nav.adoc 6 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/changelog.adoc: -------------------------------------------------------------------------------- 1 | = Changelog 2 | 3 | [[release-0.18.0]] 4 | == 0.18.0 (UNRELEASED) 5 | 6 | [[release-0.17.1]] 7 | == 0.17.1 (2025-02-10) 8 | 9 | === Fixes 10 | 11 | * Fix a memory leak when loading a Pkl module (https://github.com/apple/pkl-spring/pull/11[#11]). 12 | * Fix an issue when loading a file-based config source (https://github.com/apple/pkl-spring/pull/11[#11]). 13 | 14 | === Contributors ❤️ 15 | 16 | Thank you to all our contributors! 17 | 18 | * https://github.com/protobufel2[@protobufel2] 19 | 20 | [[release-0.17.0]] 21 | == 0.17.0 (2025-01-24) 22 | 23 | === Changes 24 | 25 | * Update to Pkl 0.27.0 (https://github.com/apple/pkl-spring/pull/6[#6]). 26 | * Update samples to Spring Boot 3.x (https://github.com/apple/pkl-spring/pull/6[#6]). 27 | 28 | [NOTE] 29 | ==== 30 | Starting with Pkl 0.27, Pkl's Java and Kotlin code generators target Spring Boot 3. 31 | 32 | To use _pkl-spring_ with Spring Boot 3, the following requirements must be met: 33 | 34 | * Config classes are generated with Pkl 0.27 or later. 35 | Typically, this is done by using version 0.27.0 or later of Pkl's Gradle plugin. 36 | * (Java) Config classes are compiled with the `-parameters` compiler argument. 37 | Spring Boot's Gradle plugin handles this automatically. 38 | * (Kotlin) Config classes are compiled with the `-java-parameters` compiler argument. 39 | Spring Boot's Gradle plugin handles this automatically. 40 | 41 | To use _pkl-spring_ with Spring Boot 2, the following requirements must be met: 42 | 43 | * Config classes are generated with Pkl 0.26 or earlier. 44 | Typically, this is done by using version 0.26.3 or earlier of Pkl's Gradle plugin. 45 | ==== 46 | 47 | === Contributors ❤️ 48 | 49 | Thank you to all our contributors! 50 | 51 | * https://github.com/odenix[@odenix] 52 | 53 | [[release-0.16.0]] 54 | == 0.16.0 (2024-06-17) 55 | 56 | * Update to Pkl 0.26 57 | * Update to Java 17 58 | 59 | [[release-0.15.0]] 60 | == 0.15.0 (2024-02-01) 61 | 62 | Initial library release 63 | 64 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/index.adoc: -------------------------------------------------------------------------------- 1 | = Spring Boot Integration 2 | :uri-issue: https://github.com/apple/pkl-spring/issues 3 | 4 | _pkl-spring_ is a Spring Boot extension for configuring Boot apps with Pkl. 5 | 6 | Because _pkl-spring_ plugs into Spring Boot's standard configuration mechanism, 7 | configuring your Boot app with Pkl works much the same as configuring it with Java properties or YAML. 8 | 9 | == Contact 10 | 11 | * Raise an {uri-issue}[Issue] 12 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/installation.adoc: -------------------------------------------------------------------------------- 1 | = Installation 2 | 3 | :uri-pkl-spring-module: https://central.sonatype.com/artifact/org.pkl-lang/pkl-spring 4 | 5 | The _pkl-spring_ library is available {uri-pkl-spring-module}[from Maven Central]. 6 | It requires Java 17 or later and Spring Boot 2.2 or later. 7 | 8 | [NOTE] 9 | ==== 10 | The _pkl-spring_ POM does not declare a dependency on Spring Boot; 11 | this is left to your application. 12 | ==== 13 | 14 | == Gradle 15 | 16 | To use the library in a Gradle project, declare the following dependency: 17 | 18 | [tabs] 19 | ==== 20 | Kotlin:: 21 | + 22 | .build.gradle.kts 23 | [source,kotlin,subs="+attributes"] 24 | ---- 25 | dependencies { 26 | implementation("org.pkl-lang:pkl-spring:{page-version}") 27 | } 28 | 29 | repositories { 30 | mavenCentral() 31 | } 32 | ---- 33 | 34 | Groovy:: 35 | + 36 | .build.gradle 37 | [source,groovy,subs="+attributes"] 38 | ---- 39 | dependencies { 40 | implementation "org.pkl-lang:pkl-spring:{page-version}" 41 | } 42 | 43 | repositories { 44 | mavenCentral() 45 | } 46 | ---- 47 | ==== 48 | 49 | == Maven 50 | 51 | To use the library in a Maven project, declare the following dependency: 52 | 53 | [source,xml,subs="+attributes"] 54 | ---- 55 | 56 | 57 | org.pkl-lang 58 | pkl-spring 59 | {page-version} 60 | 61 | 62 | ---- 63 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/usage.adoc: -------------------------------------------------------------------------------- 1 | = Usage 2 | 3 | :uri-externalized-configuration: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html 4 | :uri-type-safe-config-properties: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties 5 | :uri-application-property-file: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-application-property-files 6 | :uri-configuration-properties: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/context/properties/ConfigurationProperties.html 7 | :uri-environment: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/env/Environment.html 8 | :uri-boot-sample-sources: https://github.com/apple/pkl-spring/blob/main/samples/spring-boot 9 | :uri-kotlin-sample-sources: https://github.com/apple/pkl-spring/blob/main/samples/spring-boot-kotlin 10 | 11 | Let's walk through an example for configuring a Spring Boot application with Pkl. 12 | 13 | [NOTE] 14 | ==== 15 | The source code for this example is available on link:{uri-boot-sample-sources}[GitHub]. 16 | The equivalent Kotlin example is link:{uri-kotlin-sample-sources}[here]. 17 | 18 | For background information, 19 | see {uri-externalized-configuration}[Externalized Configuration], 20 | in particular {uri-type-safe-config-properties}[Type-safe Configuration Properties], 21 | in the Spring Boot documentation. 22 | ==== 23 | 24 | . [[schema]] Define a configuration xref:main:language-reference:index.adoc#classes[schema] in `src/main/resources`: 25 | + 26 | [source,{pkl}] 27 | .AppConfig.pkl 28 | ---- 29 | // this module name determines the package and 30 | // class name of the generated Java config class 31 | module samples.boot.AppConfig 32 | 33 | server: Server 34 | 35 | class Server { 36 | endpoints: Listing 37 | } 38 | 39 | class Endpoint { 40 | name: String 41 | port: UInt16 42 | } 43 | ---- 44 | 45 | . Define an {uri-application-property-file}[Application Property File] next to the schema: 46 | + 47 | [source,{pkl}] 48 | .application.pkl 49 | ---- 50 | amends "modulepath:/appConfig.pkl" 51 | 52 | server { 53 | endpoints { 54 | new { 55 | name = "endpoint1" 56 | port = 1234 57 | } 58 | new { 59 | name = "endpoint2" 60 | port = 5678 61 | } 62 | } 63 | } 64 | ---- 65 | 66 | . Use Pkl's xref:main:pkl-gradle:index.adoc[Gradle plugin] to generate Java config classes from the schema. 67 | Generated classes are placed in `generated/configClasses/` and compiled together with your production code. 68 | To run the generator directly (which isn't typically necessary), run command `./gradlew configClasses`. 69 | + 70 | [NOTE] 71 | ==== 72 | This example uses Spring Boot 3. 73 | 74 | To use _pkl-spring_ with Spring Boot 3, the following requirements must be met: 75 | 76 | * Config classes are generated with Pkl 0.27.0 or later. 77 | Typically, this is done by using version 0.27.0 or later of Pkl's Gradle plugin. 78 | * (Java) Config classes are compiled with the `-parameters` compiler argument. 79 | Spring Boot's Gradle plugin handles this automatically. 80 | * (Kotlin) Config classes are compiled with the `-java-parameters` compiler argument. 81 | Spring Boot's Gradle plugin handles this automatically. 82 | 83 | To use _pkl-spring_ with Spring Boot 2, the following requirements must be met: 84 | 85 | * Config classes are generated with Pkl 0.26 or earlier. 86 | Typically, this is done by using version 0.26.x or earlier of Pkl's Gradle plugin. 87 | ==== 88 | + 89 | [tabs] 90 | ==== 91 | Groovy:: 92 | + 93 | .build.gradle 94 | [source,groovy] 95 | ---- 96 | plugins { 97 | id "org.pkl-lang" version "$pklVersion" 98 | } 99 | 100 | pkl { 101 | javaCodeGenerators { 102 | configClasses { 103 | generateGetters.set(true) 104 | generateSpringBootConfig.set(true) 105 | sourceModules.set(files("src/main/resources/AppConfig.pkl")) 106 | } 107 | } 108 | } 109 | ---- 110 | + 111 | .settings.gradle 112 | [source,groovy] 113 | ---- 114 | pluginManagement { 115 | repositories { 116 | mavenCentral() 117 | } 118 | } 119 | ---- 120 | 121 | Kotlin:: 122 | + 123 | .build.gradle.kts 124 | [source,kotlin] 125 | ---- 126 | plugins { 127 | id("org.pkl-lang") version "$pklVersion" 128 | } 129 | 130 | pkl { 131 | javaCodeGenerators { 132 | register("configClasses") { 133 | generateGetters.set(true) 134 | generateSpringBootConfig.set(true) 135 | sourceModules.set(files("src/main/resources/AppConfig.pkl")) 136 | } 137 | } 138 | } 139 | ---- 140 | + 141 | .settings.gradle.kts 142 | [source,kotlin] 143 | ---- 144 | pluginManagement { 145 | repositories { 146 | mavenCentral() 147 | } 148 | } 149 | ---- 150 | ==== 151 | 152 | . Annotate your Boot application class with `@ConfigurationPropertiesScan`. 153 | (Alternatively, explicitly list configuration classes with `@EnableConfigurationProperties(...)`.) 154 | + 155 | [source,java] 156 | ---- 157 | @SpringBootApplication 158 | @ConfigurationPropertiesScan 159 | public class Application { ... } 160 | ---- 161 | 162 | . Inject the generated config classes into your application's components as appropriate. 163 | + 164 | [source,java] 165 | ---- 166 | @Service 167 | public class Server { 168 | public Server(AppConfig.Server config) { ... } 169 | } 170 | ---- 171 | 172 | . To get access to the entire configuration, inject the `AppConfig` class itself: 173 | + 174 | [source,java] 175 | ---- 176 | @Service 177 | public class Server { 178 | public Server(AppConfig config) { ... } 179 | } 180 | ---- 181 | 182 | This example demonstrates one way of configuring Spring Boot applications with Pkl. 183 | Some possible deviations are: 184 | 185 | * Use multiple config files, for example one per environment. 186 | * Read config files from the file system instead of the class path. 187 | * Write config classes by hand instead of generating them. 188 | * Do not define a config schema. (Config classes cannot be generated in this case.) 189 | * Use a build tool other than Gradle. 190 | ** Declare a build dependency on `pkl-codegen-java`. 191 | ** Invoke the code generator's `main` method during the build, passing the required arguments. 192 | ** Configure the build to compile generated config classes together with your production code. 193 | ** For more information, see the xref:main:java-binding:codegen.adoc[Java code generator] docs. 194 | -------------------------------------------------------------------------------- /docs/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:spring:ROOT:installation.adoc[Installation] 2 | * xref:spring:ROOT:changelog.adoc[Changelog] 3 | * xref:ROOT:usage.adoc[Usage] 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | 3 | org.gradle.parallel=true 4 | 5 | version=0.18.0 6 | group=org.pkl-lang 7 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | # Note: This version catalog is used both for main and samples build. 2 | 3 | [versions] 4 | # https://central.sonatype.com/artifact/org.assertj/assertj-core/versions 5 | assertJ = "3.26.3" 6 | # https://github.com/google/google-java-format/releases/ 7 | googleJavaFormat = "1.24.0" 8 | # only used in Kotlin sample 9 | # https://central.sonatype.com/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8/versions 10 | kotlin = "2.0.21" 11 | # https://github.com/facebook/ktfmt/releases/ 12 | ktfmt = "0.52" 13 | # https://central.sonatype.com/artifact/org.junit.jupiter/junit-jupiter/versions 14 | junit = "5.11.3" 15 | pkl = "0.27.0" 16 | # only used for testing; use same version as Spring Boot 17 | # https://central.sonatype.com/artifact/org.springframework.boot/spring-boot/dependencies 18 | spring = "6.1.13" 19 | # https://central.sonatype.com/artifact/org.springframework.boot/spring-boot/versions 20 | springBoot = "3.3.4" 21 | # https://central.sonatype.com/artifact/com.github.spotbugs/spotbugs-annotations/versions 22 | spotbugsAnnotations = "4.9.0" 23 | # https://github.com/diffplug/spotless/releases 24 | spotlessPlugin = "6.25.0" 25 | # https://github.com/gradle-nexus/publish-plugin/releases/ 26 | nexusPublishPlugin = "2.0.0" 27 | 28 | [libraries] 29 | assertJ = { group = "org.assertj", name = "assertj-core", version.ref = "assertJ" } 30 | junitApi = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } 31 | junitEngine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } 32 | junitParams = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" } 33 | kotlinStdLib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } 34 | kotlinReflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } 35 | pklConfigJavaAll = { group = "org.pkl-lang", name = "pkl-config-java-all", version.ref = "pkl" } 36 | pklSpring = { group = "org.pkl-lang", name = "pkl-spring", version.ref = "pkl" } 37 | spotlessPlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessPlugin"} 38 | springBoot = { group = "org.springframework.boot", name = "spring-boot", version.ref = "springBoot" } 39 | springBootAutoConfigure = { group = "org.springframework.boot", name = "spring-boot-autoconfigure", version.ref = "springBoot" } 40 | springBootTest = { group = "org.springframework.boot", name = "spring-boot-test", version.ref = "springBoot" } 41 | springTest = { group = "org.springframework", name = "spring-test", version.ref = "spring" } 42 | spotbugsAnnotations = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugsAnnotations" } 43 | 44 | [plugins] 45 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 46 | pkl = { id = "org.pkl-lang", version.ref = "pkl" } 47 | nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublishPlugin" } 48 | spotless = { id = "com.diffplug.spotless", version.ref = "spotlessPlugin" } 49 | springBoot = { id = "org.springframework.boot", version.ref = "springBoot" } 50 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/pkl-spring/842ef7e8bddd21ac1f8f801db2578af7a094e3e7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /samples/gradle.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | 3 | org.gradle.parallel=true 4 | 5 | version = 0.16.0 6 | group = "org.pkl-lang.spring.samples" 7 | -------------------------------------------------------------------------------- /samples/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/pkl-spring/842ef7e8bddd21ac1f8f801db2578af7a094e3e7/samples/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /samples/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /samples/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /samples/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "samples" 2 | 3 | include("spring-boot") 4 | include("spring-boot-kotlin") 5 | include("spring-boot-external-config") 6 | 7 | includeBuild("../") 8 | 9 | pluginManagement { 10 | // To develop against a local Pkl build's Pkl Gradle plugin, uncomment the next line. 11 | //includeBuild("../../pkl") 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | @Suppress("UnstableApiUsage") 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | // use same version catalog as main build 25 | versionCatalogs { 26 | create("libs") { 27 | from(files("../gradle/libs.versions.toml")) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/spring-boot-external-config/README.adoc: -------------------------------------------------------------------------------- 1 | = Spring Boot External Config Example 2 | :uri-docs: https://pkl-lang.org/spring/current/spring-boot.html 3 | 4 | This example demonstrates how to configure a Spring Boot application when the configuration files are placed _outside_ of the resources directory. 5 | 6 | The link:src/main/resources/application.properties[resources/application.properties] file is a 7 | typical Spring Boot config file importing link:config/application.pkl[config/application.pkl] from 8 | external location. 9 | 10 | Note the use of `amends "AppConfig.pkl"` in link:config/application.pkl[config/application.pkl] 11 | which not only allows for the validation but also for the IDE code completion! 12 | 13 | For a walkthrough, see the link:{uri-docs}[documentation]. 14 | 15 | To run the application, type `../gradlew run`. 16 | To open the project in IntelliJ IDEA, go to File->Open and select the `samples` directory. -------------------------------------------------------------------------------- /samples/spring-boot-external-config/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | alias(libs.plugins.pkl) 4 | alias(libs.plugins.springBoot) 5 | } 6 | 7 | java { 8 | sourceCompatibility = JavaVersion.VERSION_17 9 | } 10 | 11 | dependencies { 12 | implementation(libs.springBoot) 13 | implementation(libs.springBootTest) 14 | implementation(libs.springBootAutoConfigure) 15 | implementation(libs.pklSpring) 16 | 17 | testImplementation(libs.junitApi) 18 | testImplementation(libs.assertJ) 19 | } 20 | 21 | pkl { 22 | javaCodeGenerators { 23 | register("configClasses") { 24 | generateGetters.set(true) 25 | generateSpringBootConfig.set(true) 26 | sourceModules.set(files("config/AppConfig.pkl")) 27 | } 28 | } 29 | } 30 | 31 | tasks.check { 32 | dependsOn(tasks.named("bootRun")) 33 | } 34 | -------------------------------------------------------------------------------- /samples/spring-boot-external-config/config/AppConfig.pkl: -------------------------------------------------------------------------------- 1 | // this module name determines the package and 2 | // class name of the generated Java config class 3 | module samples.boot.AppConfig 4 | 5 | server: Server 6 | 7 | class Server { 8 | endpoints: Listing 9 | } 10 | 11 | class Endpoint { 12 | name: String 13 | port: UInt16 14 | } 15 | -------------------------------------------------------------------------------- /samples/spring-boot-external-config/config/application.pkl: -------------------------------------------------------------------------------- 1 | amends "AppConfig.pkl" 2 | 3 | server { 4 | endpoints { 5 | new { 6 | name = "endpoint1" 7 | port = 1234 8 | } 9 | new { 10 | name = "endpoint2" 11 | port = 5678 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/spring-boot-external-config/src/main/java/samples/boot/Application.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package samples.boot; 17 | 18 | import org.springframework.boot.CommandLineRunner; 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 22 | import org.springframework.context.ApplicationContext; 23 | import org.springframework.context.annotation.Bean; 24 | 25 | @SpringBootApplication 26 | @ConfigurationPropertiesScan 27 | public class Application { 28 | public static void main(String... args) { 29 | new SpringApplication(Application.class).run(args); 30 | } 31 | 32 | @Bean 33 | @SuppressWarnings("unused") 34 | public CommandLineRunner commandLineRunner(ApplicationContext ctx) { 35 | return args -> { 36 | var server = ctx.getBean(Server.class); 37 | System.out.println(server.getConfig()); 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /samples/spring-boot-external-config/src/main/java/samples/boot/Server.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package samples.boot; 17 | 18 | import org.springframework.stereotype.Service; 19 | 20 | @Service 21 | public class Server { 22 | private final AppConfig.Server config; 23 | 24 | public Server(AppConfig.Server config) { 25 | this.config = config; 26 | } 27 | 28 | public AppConfig.Server getConfig() { 29 | return config; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/spring-boot-external-config/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.config.import=file:./config/application.pkl 2 | -------------------------------------------------------------------------------- /samples/spring-boot-kotlin/README.adoc: -------------------------------------------------------------------------------- 1 | = Spring Boot Kotlin Example 2 | :uri-docs: https://pkl-lang.org/spring/current/spring-boot.html 3 | 4 | This example demonstrates how to configure a Spring Boot application written in Kotlin with Pkl. 5 | For a walkthrough of the corresponding Java example, see the link:{uri-docs}[documentation]. 6 | 7 | To run the application, type `../gradlew run`. 8 | To open the project in IntelliJ IDEA, go to File->Open and select the `samples` directory. -------------------------------------------------------------------------------- /samples/spring-boot-kotlin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.pkl) 3 | alias(libs.plugins.kotlin) 4 | alias(libs.plugins.springBoot) 5 | } 6 | 7 | dependencies { 8 | implementation(libs.kotlinStdLib) 9 | implementation(libs.springBoot) 10 | implementation(libs.springBootTest) 11 | implementation(libs.springBootAutoConfigure) 12 | implementation(libs.pklSpring) 13 | 14 | runtimeOnly(libs.kotlinReflect) 15 | 16 | testImplementation(libs.junitApi) 17 | testImplementation(libs.assertJ) 18 | } 19 | 20 | pkl { 21 | kotlinCodeGenerators { 22 | register("configClasses") { 23 | generateSpringBootConfig.set(true) 24 | sourceModules.set(files("src/main/resources/AppConfig.pkl")) 25 | } 26 | } 27 | } 28 | 29 | tasks.check { 30 | dependsOn(tasks.named("bootRun")) 31 | } 32 | -------------------------------------------------------------------------------- /samples/spring-boot-kotlin/src/main/kotlin/samples/kotlin/Application.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package samples.kotlin 17 | 18 | import org.springframework.boot.CommandLineRunner 19 | import org.springframework.boot.SpringApplication 20 | import org.springframework.boot.autoconfigure.SpringBootApplication 21 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan 22 | import org.springframework.context.ApplicationContext 23 | import org.springframework.context.annotation.Bean 24 | 25 | @SpringBootApplication 26 | @ConfigurationPropertiesScan 27 | open class Application { 28 | @Bean 29 | @Suppress("unused") 30 | open fun commandLineRunner(ctx: ApplicationContext): CommandLineRunner { 31 | return CommandLineRunner { 32 | val server = ctx.getBean(Server::class.java) 33 | println(server.config) 34 | } 35 | } 36 | 37 | companion object { 38 | @JvmStatic 39 | fun main(args: Array) { 40 | SpringApplication(Application::class.java).run(*args) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/spring-boot-kotlin/src/main/kotlin/samples/kotlin/Server.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package samples.kotlin 17 | 18 | import org.springframework.stereotype.Service 19 | 20 | @Service class Server(val config: AppConfig.Server) 21 | -------------------------------------------------------------------------------- /samples/spring-boot-kotlin/src/main/resources/AppConfig.pkl: -------------------------------------------------------------------------------- 1 | // this module name determines the package and 2 | // class name of the generated Kotlin config class 3 | module samples.kotlin.AppConfig 4 | 5 | server: Server 6 | 7 | class Server { 8 | endpoints: Listing 9 | } 10 | 11 | class Endpoint { 12 | name: String 13 | port: UInt16 14 | } 15 | -------------------------------------------------------------------------------- /samples/spring-boot-kotlin/src/main/resources/application.pkl: -------------------------------------------------------------------------------- 1 | amends "AppConfig.pkl" 2 | 3 | server { 4 | endpoints { 5 | new { 6 | name = "endpoint1" 7 | port = 1234 8 | } 9 | new { 10 | name = "endpoint2" 11 | port = 5678 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/spring-boot/README.adoc: -------------------------------------------------------------------------------- 1 | = Spring Boot Example 2 | :uri-docs: https://pkl-lang.org/spring/current/spring-boot.html 3 | 4 | This example demonstrates how to configure a Spring Boot application with Pkl. 5 | For a walkthrough, see the link:{uri-docs}[documentation]. 6 | 7 | To run the application, type `../gradlew run`. 8 | To open the project in IntelliJ IDEA, go to File->Open and select the `samples` directory. -------------------------------------------------------------------------------- /samples/spring-boot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | alias(libs.plugins.pkl) 4 | alias(libs.plugins.springBoot) 5 | } 6 | 7 | java { 8 | sourceCompatibility = JavaVersion.VERSION_17 9 | } 10 | 11 | dependencies { 12 | implementation(libs.springBoot) 13 | implementation(libs.springBootTest) 14 | implementation(libs.springBootAutoConfigure) 15 | implementation(libs.pklSpring) 16 | 17 | testImplementation(libs.junitApi) 18 | testImplementation(libs.assertJ) 19 | } 20 | 21 | pkl { 22 | javaCodeGenerators { 23 | register("configClasses") { 24 | generateGetters.set(true) 25 | generateSpringBootConfig.set(true) 26 | sourceModules.set(files("src/main/resources/AppConfig.pkl")) 27 | } 28 | } 29 | } 30 | 31 | tasks.check { 32 | dependsOn(tasks.named("bootRun")) 33 | } 34 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/java/samples/boot/Application.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package samples.boot; 17 | 18 | import org.springframework.boot.CommandLineRunner; 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 22 | import org.springframework.context.ApplicationContext; 23 | import org.springframework.context.annotation.Bean; 24 | 25 | @SpringBootApplication 26 | @ConfigurationPropertiesScan 27 | public class Application { 28 | public static void main(String... args) { 29 | new SpringApplication(Application.class).run(args); 30 | } 31 | 32 | @Bean 33 | @SuppressWarnings("unused") 34 | public CommandLineRunner commandLineRunner(ApplicationContext ctx) { 35 | return args -> { 36 | var server = ctx.getBean(Server.class); 37 | System.out.println(server.getConfig()); 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/java/samples/boot/Server.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package samples.boot; 17 | 18 | import org.springframework.stereotype.Service; 19 | 20 | @Service 21 | public class Server { 22 | private final AppConfig.Server config; 23 | 24 | public Server(AppConfig.Server config) { 25 | this.config = config; 26 | } 27 | 28 | public AppConfig.Server getConfig() { 29 | return config; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/resources/AppConfig.pkl: -------------------------------------------------------------------------------- 1 | // this module name determines the package and 2 | // class name of the generated Java config class 3 | module samples.boot.AppConfig 4 | 5 | server: Server 6 | 7 | class Server { 8 | endpoints: Listing 9 | } 10 | 11 | class Endpoint { 12 | name: String 13 | port: UInt16 14 | } 15 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/resources/application.pkl: -------------------------------------------------------------------------------- 1 | amends "AppConfig.pkl" 2 | 3 | server { 4 | endpoints { 5 | new { 6 | name = "endpoint1" 7 | port = 1234 8 | } 9 | new { 10 | name = "endpoint2" 11 | port = 5678 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "pkl-spring" 2 | 3 | pluginManagement { 4 | // To develop against a local Pkl build's Pkl Gradle plugin, uncomment the next line. 5 | //includeBuild("../pkl") 6 | 7 | repositories { 8 | mavenCentral() 9 | gradlePluginPortal() 10 | } 11 | } 12 | 13 | @Suppress("UnstableApiUsage") 14 | dependencyResolutionManagement { 15 | repositories { 16 | // only use repositories specified here 17 | // https://github.com/gradle/gradle/issues/15732 18 | repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) 19 | mavenCentral() 20 | } 21 | } 22 | 23 | val javaVersion = JavaVersion.current() 24 | require(javaVersion.isCompatibleWith(JavaVersion.VERSION_17)) { 25 | "Project requires Java 17 or higher, but found ${javaVersion.majorVersion}." 26 | } 27 | 28 | //includeBuild("../pkl") 29 | // To develop against a local Pkl build's pkl-config-java library, 30 | // uncomment the above line and replace `name = "pkl-config-java-all"` 31 | // with `name = "pkl-config-java"` in libs.versions.toml. 32 | // Editing libs.versions.toml is necessary because I couldn't get 33 | // `includeBuild(../pkl) { dependencySubstitution {...} }` to work for pkl-config-java-all. 34 | -------------------------------------------------------------------------------- /src/main/java/org/pkl/spring/boot/PklAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.pkl.spring.boot; 17 | 18 | import java.util.*; 19 | import org.pkl.core.PNull; 20 | import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; 21 | import org.springframework.context.annotation.Configuration; 22 | import org.springframework.core.convert.TypeDescriptor; 23 | import org.springframework.core.convert.converter.GenericConverter; 24 | import org.springframework.core.env.ConfigurableEnvironment; 25 | import org.springframework.lang.Nullable; 26 | import org.springframework.stereotype.Component; 27 | 28 | @Configuration 29 | public class PklAutoConfiguration { 30 | public PklAutoConfiguration(ConfigurableEnvironment env) { 31 | // otherwise `Environment.getProperty("pklPropertyWithNullValue")` fails with 32 | // `ConverterNotFoundException` 33 | env.getConversionService().addConverter(new PNullConverter()); 34 | } 35 | 36 | @Component 37 | @SuppressWarnings("unused") 38 | @ConfigurationPropertiesBinding 39 | public static class PNullConverter implements GenericConverter { 40 | @Override 41 | public @Nullable Set getConvertibleTypes() { 42 | return Set.of(new ConvertiblePair(PNull.class, Object.class)); 43 | } 44 | 45 | @Override 46 | public @Nullable Object convert( 47 | @Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { 48 | assert source == PNull.getInstance(); 49 | return targetType.getType() == Optional.class ? Optional.empty() : null; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/pkl/spring/boot/PklPropertySourceLoader.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.pkl.spring.boot; 17 | 18 | import java.io.IOException; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.*; 21 | import org.pkl.core.*; 22 | import org.springframework.boot.env.PropertySourceLoader; 23 | import org.springframework.core.env.MapPropertySource; 24 | import org.springframework.core.env.PropertySource; 25 | import org.springframework.core.io.Resource; 26 | 27 | public class PklPropertySourceLoader implements PropertySourceLoader { 28 | 29 | @Override 30 | public String[] getFileExtensions() { 31 | return new String[] {"pkl", "pcf"}; 32 | } 33 | 34 | @Override 35 | public List> load(String propertySourceName, Resource resource) 36 | throws IOException { 37 | PModule module; 38 | try (var evaluator = EvaluatorBuilder.preconfigured().build()) { 39 | module = evaluator.evaluate(toModuleSource(resource)); 40 | } 41 | 42 | var result = new LinkedHashMap(); 43 | module.getProperties().forEach((name, value) -> flatten(name, value, result)); 44 | return List.of(new MapPropertySource(propertySourceName, result)); 45 | } 46 | 47 | private ModuleSource toModuleSource(Resource resource) throws IOException { 48 | if (resource.isFile()) { 49 | return ModuleSource.file(resource.getFile()); 50 | } 51 | var text = resource.getContentAsString(StandardCharsets.UTF_8); 52 | return ModuleSource.create(resource.getURI(), text); 53 | } 54 | 55 | private static void flatten( 56 | String propertyName, Object propertyValue, Map result) { 57 | if (propertyValue instanceof Composite composite) { 58 | flatten(propertyName, composite.getProperties(), result); 59 | } else if (propertyValue instanceof Map map) { 60 | if (map.isEmpty()) { 61 | result.put(propertyName, Collections.emptyMap()); 62 | } else { 63 | map.forEach((name, value) -> flatten(propertyName + '.' + name, value, result)); 64 | } 65 | } else if (propertyValue instanceof Collection collection) { 66 | if (collection.isEmpty()) { 67 | result.put( 68 | propertyName, 69 | propertyValue instanceof Set ? Collections.emptySet() : Collections.emptyList()); 70 | } else { 71 | var index = 0; 72 | for (var element : collection) { 73 | flatten(propertyName + '[' + index + ']', element, result); 74 | index++; 75 | } 76 | } 77 | } else { 78 | result.put(propertyName, propertyValue); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/org/pkl/spring/boot/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | package org.pkl.spring.boot; 3 | 4 | import org.springframework.lang.NonNullApi; 5 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.env.PropertySourceLoader=org.pkl.spring.boot.PklPropertySourceLoader 2 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.pkl.spring.boot.PklAutoConfiguration 3 | -------------------------------------------------------------------------------- /src/test/java/org/pkl/spring/boot/ConfigTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.pkl.spring.boot; 17 | 18 | import static org.assertj.core.api.Assertions.*; 19 | 20 | import java.util.*; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.ExtendWith; 23 | import org.pkl.core.*; 24 | import org.springframework.beans.factory.annotation.Autowired; 25 | import org.springframework.boot.test.context.SpringBootTest; 26 | import org.springframework.context.ApplicationContext; 27 | import org.springframework.core.convert.ConverterNotFoundException; 28 | import org.springframework.core.env.Environment; 29 | import org.springframework.test.context.junit.jupiter.SpringExtension; 30 | 31 | @ExtendWith(SpringExtension.class) 32 | @SpringBootTest(classes = {ConfigTestApp.class}) 33 | public class ConfigTest { 34 | private static final AppConfig.Person pigeon = new AppConfig.Person("Pigeon", 42, List.of()); 35 | private static final AppConfig.Person parrot = new AppConfig.Person("Parrot", 21, List.of()); 36 | 37 | @Autowired 38 | @SuppressWarnings("unused") 39 | private Environment environment; 40 | 41 | @Autowired 42 | @SuppressWarnings("unused") 43 | private ApplicationContext appContext; 44 | 45 | @Autowired 46 | @SuppressWarnings("unused") 47 | private AppConfig appConfig; 48 | 49 | @Test 50 | public void consumeIntProperty() { 51 | assertThat(environment.getRequiredProperty("intProp")).isEqualTo("42"); 52 | assertThat(environment.getRequiredProperty("intProp", long.class)).isEqualTo(42L); 53 | 54 | assertThat(environment.getRequiredProperty("int32")).isEqualTo("-42"); 55 | assertThat(environment.getRequiredProperty("int32", int.class)).isEqualTo(-42); 56 | } 57 | 58 | @Test 59 | public void consumeFloatProperty() { 60 | assertThat(environment.getRequiredProperty("floatProp")).isEqualTo("1.23"); 61 | assertThat(environment.getRequiredProperty("floatProp", double.class)).isEqualTo(1.23d); 62 | } 63 | 64 | @Test 65 | public void consumeBooleanProperty() { 66 | assertThat(environment.getRequiredProperty("booleanProp")).isEqualTo("true"); 67 | assertThat(environment.getRequiredProperty("booleanProp", boolean.class)).isTrue(); 68 | } 69 | 70 | @Test 71 | public void consumeStringProperty() { 72 | assertThat(environment.getRequiredProperty("string")).isEqualTo("string"); 73 | assertThat(environment.getRequiredProperty("string", String.class)).isEqualTo("string"); 74 | } 75 | 76 | @Test 77 | public void consumeDurationProperty() { 78 | assertThatThrownBy(() -> environment.getRequiredProperty("duration")) 79 | .isInstanceOf(ConverterNotFoundException.class); 80 | assertThat(environment.getRequiredProperty("duration", Duration.class)) 81 | .isEqualTo(new Duration(3, DurationUnit.HOURS)); 82 | } 83 | 84 | @Test 85 | public void consumeDataSizeProperty() { 86 | assertThatThrownBy(() -> environment.getRequiredProperty("dataSize")) 87 | .isInstanceOf(ConverterNotFoundException.class); 88 | assertThat(environment.getRequiredProperty("dataSize", DataSize.class)) 89 | .isEqualTo(new DataSize(1.23, DataSizeUnit.GIGABYTES)); 90 | } 91 | 92 | @Test 93 | public void consumePairProperty() { 94 | assertThatThrownBy(() -> environment.getRequiredProperty("pair")) 95 | .isInstanceOf(ConverterNotFoundException.class); 96 | assertThat(environment.getRequiredProperty("pair", Pair.class)) 97 | .isEqualTo(new Pair<>("hello", true)); 98 | } 99 | 100 | @Test 101 | public void consumeRegexProperty() { 102 | // Boot 3 no longer requires `getRequiredType("regex", Pattern.class)` 103 | assertThat(environment.getRequiredProperty("regex")).isEqualTo("regex"); 104 | } 105 | 106 | @Test 107 | @SuppressWarnings("unchecked") 108 | public void consumeNullableIntProperty() { 109 | assertThat(environment.getRequiredProperty("nullableInt1")).isEqualTo("42"); 110 | assertThat(environment.getRequiredProperty("nullableInt1", long.class)).isEqualTo(42L); 111 | assertThat(environment.getRequiredProperty("nullableInt1", Optional.class)).contains(42L); 112 | 113 | assertThat(environment.containsProperty("nullableInt2")).isTrue(); 114 | assertThat(environment.getProperty("nullableInt2")).isNull(); 115 | assertThat(environment.getProperty("nullableInt2", Long.class)).isNull(); 116 | assertThat(environment.getProperty("nullableInt2", Optional.class)).isEmpty(); 117 | } 118 | 119 | @Test 120 | @SuppressWarnings("unchecked") 121 | public void consumeNullableStringProperty() { 122 | assertThat(environment.getRequiredProperty("nullableString1")).isEqualTo("string"); 123 | assertThat(environment.getRequiredProperty("nullableString1", String.class)) 124 | .isEqualTo("string"); 125 | assertThat(environment.getRequiredProperty("nullableString1", Optional.class)) 126 | .contains("string"); 127 | 128 | assertThat(environment.containsProperty("nullableString2")).isTrue(); 129 | assertThat(environment.getProperty("nullableString2")).isNull(); 130 | assertThat(environment.getProperty("nullableString2", String.class)).isNull(); 131 | assertThat(environment.getProperty("nullableString2", Optional.class)).isEmpty(); 132 | } 133 | 134 | @Test 135 | @SuppressWarnings("unchecked") 136 | public void consumeNullableTypedProperties() { 137 | assertThat(environment.getRequiredProperty("nullablePigeon1.name")).isEqualTo("Pigeon"); 138 | assertThat(environment.getRequiredProperty("nullablePigeon1.name", String.class)) 139 | .isEqualTo("Pigeon"); 140 | assertThat(environment.getRequiredProperty("nullablePigeon1.name", Optional.class)) 141 | .contains("Pigeon"); 142 | 143 | assertThat(environment.getRequiredProperty("nullablePigeon1.age")).isEqualTo("42"); 144 | assertThat(environment.getRequiredProperty("nullablePigeon1.age", long.class)).isEqualTo(42L); 145 | assertThat(environment.getRequiredProperty("nullablePigeon1.age", Optional.class)) 146 | .contains(42L); 147 | 148 | assertThat(environment.containsProperty("nullablePigeon2")).isTrue(); 149 | assertThat(environment.getProperty("nullablePigeon2")).isNull(); 150 | assertThat(environment.getProperty("nullablePigeon2", Optional.class)).isEmpty(); 151 | } 152 | 153 | @Test 154 | public void consumeNullableListProperties() { 155 | doConsumeNullableListOrListingProperties("listWithNullElements"); 156 | } 157 | 158 | @Test 159 | @SuppressWarnings("unchecked") 160 | public void consumeNullableSetProperties() { 161 | assertThat(environment.getRequiredProperty("setWithNullElements[0]")).isEqualTo("elem1"); 162 | assertThat(environment.getRequiredProperty("setWithNullElements[0]", String.class)) 163 | .isEqualTo("elem1"); 164 | assertThat(environment.getRequiredProperty("setWithNullElements[0]", Optional.class)) 165 | .contains("elem1"); 166 | 167 | assertThat(environment.getRequiredProperty("setWithNullElements[1]")).isEqualTo("elem2"); 168 | assertThat(environment.getRequiredProperty("setWithNullElements[1]", String.class)) 169 | .isEqualTo("elem2"); 170 | assertThat(environment.getRequiredProperty("setWithNullElements[1]", Optional.class)) 171 | .contains("elem2"); 172 | } 173 | 174 | @Test 175 | public void consumeNullableMapProperties() { 176 | doConsumeNullableMapOrMappingProperties("mapWithNullElements"); 177 | } 178 | 179 | @Test 180 | public void consumeNullableListingProperties() { 181 | doConsumeNullableListOrListingProperties("listingWithNullElements"); 182 | } 183 | 184 | @Test 185 | public void consumeNullableMappingProperties() { 186 | doConsumeNullableMapOrMappingProperties("mappingWithNullElements"); 187 | } 188 | 189 | @SuppressWarnings("unchecked") 190 | private void doConsumeNullableListOrListingProperties(String name) { 191 | assertThat(environment.getRequiredProperty(name + "[0]")).isEqualTo("elem1"); 192 | assertThat(environment.getRequiredProperty(name + "[0]", String.class)).isEqualTo("elem1"); 193 | assertThat(environment.getRequiredProperty(name + "[0]", Optional.class)).contains("elem1"); 194 | 195 | assertThat(environment.getRequiredProperty(name + "[1]")).isEqualTo("elem2"); 196 | assertThat(environment.getRequiredProperty(name + "[1]", String.class)).isEqualTo("elem2"); 197 | assertThat(environment.getRequiredProperty(name + "[1]", Optional.class)).contains("elem2"); 198 | 199 | assertThat(environment.containsProperty(name + "[2]")).isFalse(); 200 | } 201 | 202 | @SuppressWarnings("unchecked") 203 | private void doConsumeNullableMapOrMappingProperties(String name) { 204 | assertThat(environment.getRequiredProperty(name + ".0")).isEqualTo("elem1"); 205 | assertThat(environment.getRequiredProperty(name + ".0", String.class)).isEqualTo("elem1"); 206 | assertThat(environment.getRequiredProperty(name + ".0", Optional.class)).contains("elem1"); 207 | 208 | assertThat(environment.containsProperty(name + ".1")).isTrue(); 209 | assertThat(environment.getProperty(name + ".1")).isNull(); 210 | assertThat(environment.getProperty(name + ".1", String.class)).isNull(); 211 | assertThat(environment.getProperty(name + ".1", Optional.class)).isEmpty(); 212 | 213 | assertThat(environment.getRequiredProperty(name + ".2")).isEqualTo("elem2"); 214 | assertThat(environment.getRequiredProperty(name + ".2", String.class)).isEqualTo("elem2"); 215 | assertThat(environment.getRequiredProperty(name + ".2", Optional.class)).contains("elem2"); 216 | 217 | assertThat(environment.containsProperty(name + ".3")).isTrue(); 218 | assertThat(environment.getProperty(name + ".3")).isNull(); 219 | assertThat(environment.getProperty(name + ".3", String.class)).isNull(); 220 | assertThat(environment.getProperty(name + ".3", Optional.class)).isEmpty(); 221 | 222 | assertThat(environment.containsProperty(name + ".4")).isFalse(); 223 | } 224 | 225 | @Test 226 | public void consumeEnumProperty() { 227 | assertThat(environment.getRequiredProperty("size")).isEqualTo("SMALL"); 228 | assertThat(environment.getRequiredProperty("size", AppConfig.Size.class)) 229 | .isEqualTo(AppConfig.Size.SMALL); 230 | } 231 | 232 | @Test 233 | public void consumeListProperties() { 234 | assertThat(environment.getRequiredProperty("simpleList[0]")).isEqualTo("Pigeon"); 235 | assertThat(environment.getRequiredProperty("simpleList[1]")).isEqualTo("Parrot"); 236 | 237 | assertThat(environment.getRequiredProperty("simpleEmptyList")).isEqualTo(""); 238 | assertThat(environment.getRequiredProperty("simpleEmptyList", List.class)).isEmpty(); 239 | 240 | assertThat(environment.getRequiredProperty("complexList[0].name")).isEqualTo("Pigeon"); 241 | assertThat(environment.getRequiredProperty("complexList[0].age")).isEqualTo("42"); 242 | assertThat(environment.getRequiredProperty("complexList[1].name")).isEqualTo("Parrot"); 243 | assertThat(environment.getRequiredProperty("complexList[1].age")).isEqualTo("21"); 244 | 245 | assertThat(environment.getRequiredProperty("complexEmptyList")).isEqualTo(""); 246 | assertThat(environment.getRequiredProperty("complexEmptyList", List.class)).isEmpty(); 247 | } 248 | 249 | @Test 250 | public void consumeSetProperties() { 251 | assertThat(environment.getRequiredProperty("simpleSet[0]")).isEqualTo("Pigeon"); 252 | assertThat(environment.getRequiredProperty("simpleSet[1]")).isEqualTo("Parrot"); 253 | 254 | assertThat(environment.getRequiredProperty("simpleEmptySet")).isEqualTo(""); 255 | assertThat(environment.getRequiredProperty("simpleEmptySet", Set.class)).isEmpty(); 256 | 257 | assertThat(environment.getRequiredProperty("complexSet[0].name")).isEqualTo("Pigeon"); 258 | assertThat(environment.getRequiredProperty("complexSet[0].age")).isEqualTo("42"); 259 | assertThat(environment.getRequiredProperty("complexSet[1].name")).isEqualTo("Parrot"); 260 | assertThat(environment.getRequiredProperty("complexSet[1].age")).isEqualTo("21"); 261 | 262 | assertThat(environment.getRequiredProperty("complexEmptySet")).isEqualTo(""); 263 | assertThat(environment.getRequiredProperty("complexEmptySet", Set.class)).isEmpty(); 264 | } 265 | 266 | @Test 267 | @SuppressWarnings("unchecked") 268 | public void consumeMapProperties() { 269 | assertThat(environment.getRequiredProperty("simpleMap.Pigeon")).isEqualTo("42"); 270 | assertThat(environment.getRequiredProperty("simpleMap.Parrot")).isEqualTo("21"); 271 | 272 | assertThatThrownBy(() -> environment.getRequiredProperty("simpleEmptyMap")) 273 | .isInstanceOf(ConverterNotFoundException.class); 274 | assertThat(environment.getRequiredProperty("simpleEmptyMap", Map.class)).isEmpty(); 275 | 276 | assertThat(environment.getRequiredProperty("complexMap.Pigeon.name")).isEqualTo("Pigeon"); 277 | assertThat(environment.getRequiredProperty("complexMap.Pigeon.age")).isEqualTo("42"); 278 | assertThat(environment.getRequiredProperty("complexMap.Parrot.name")).isEqualTo("Parrot"); 279 | assertThat(environment.getRequiredProperty("complexMap.Parrot.age")).isEqualTo("21"); 280 | 281 | assertThatThrownBy(() -> environment.getRequiredProperty("complexEmptyMap")) 282 | .isInstanceOf(ConverterNotFoundException.class); 283 | assertThat(environment.getRequiredProperty("complexEmptyMap", Map.class)).isEmpty(); 284 | } 285 | 286 | @Test 287 | public void consumeListingProperties() { 288 | assertThat(environment.getRequiredProperty("simpleListing[0]")).isEqualTo("Pigeon"); 289 | assertThat(environment.getRequiredProperty("simpleListing[1]")).isEqualTo("Parrot"); 290 | 291 | assertThat(environment.getRequiredProperty("simpleEmptyListing")).isEqualTo(""); 292 | assertThat(environment.getRequiredProperty("simpleEmptyListing", List.class)).isEmpty(); 293 | 294 | assertThat(environment.getRequiredProperty("complexListing[0].name")).isEqualTo("Pigeon"); 295 | assertThat(environment.getRequiredProperty("complexListing[0].age")).isEqualTo("42"); 296 | assertThat(environment.getRequiredProperty("complexListing[1].name")).isEqualTo("Parrot"); 297 | assertThat(environment.getRequiredProperty("complexListing[1].age")).isEqualTo("21"); 298 | 299 | assertThat(environment.getRequiredProperty("complexEmptyListing")).isEqualTo(""); 300 | assertThat(environment.getRequiredProperty("complexEmptyListing", List.class)).isEmpty(); 301 | } 302 | 303 | @Test 304 | @SuppressWarnings("unchecked") 305 | public void consumeMappingProperties() { 306 | assertThat(environment.getRequiredProperty("simpleMapping.Pigeon")).isEqualTo("42"); 307 | assertThat(environment.getRequiredProperty("simpleMapping.Parrot")).isEqualTo("21"); 308 | 309 | assertThatThrownBy(() -> environment.getRequiredProperty("simpleEmptyMapping")) 310 | .isInstanceOf(ConverterNotFoundException.class); 311 | assertThat(environment.getRequiredProperty("simpleEmptyMapping", Map.class)).isEmpty(); 312 | 313 | assertThat(environment.getRequiredProperty("complexMapping.Pigeon.name")).isEqualTo("Pigeon"); 314 | assertThat(environment.getRequiredProperty("complexMapping.Pigeon.age")).isEqualTo("42"); 315 | assertThat(environment.getRequiredProperty("complexMapping.Parrot.name")).isEqualTo("Parrot"); 316 | assertThat(environment.getRequiredProperty("complexMapping.Parrot.age")).isEqualTo("21"); 317 | 318 | assertThatThrownBy(() -> environment.getRequiredProperty("complexEmptyMapping")) 319 | .isInstanceOf(ConverterNotFoundException.class); 320 | assertThat(environment.getRequiredProperty("complexEmptyMapping", Map.class)).isEmpty(); 321 | } 322 | 323 | @Test 324 | public void consumeDynamicProperties() { 325 | assertThat(environment.getRequiredProperty("dynamicPigeon.name")).isEqualTo("Pigeon"); 326 | assertThat(environment.getRequiredProperty("dynamicPigeon.age")).isEqualTo("42"); 327 | // `.0` instead of `[0]` because elements/entries of dynamic objects 328 | // are currently turned into PObject properties with String keys (see VmDynamic.export()) 329 | assertThat(environment.getRequiredProperty("dynamicPigeon.addresses.0.street")) 330 | .isEqualTo("Wilmore St."); 331 | assertThat(environment.getRequiredProperty("dynamicPigeon.addresses.0.zip")).isEqualTo("94102"); 332 | 333 | assertThat(environment.getRequiredProperty("dynamicParrot.name")).isEqualTo("Parrot"); 334 | assertThat(environment.getRequiredProperty("dynamicParrot.age")).isEqualTo("21"); 335 | // `.1` instead of `[1]` because elements/entries of dynamic objects 336 | // are currently turned into PObject properties with String keys (see VmDynamic.export()) 337 | assertThat(environment.getRequiredProperty("dynamicParrot.addresses.1.street")) 338 | .isEqualTo("Leisure St."); 339 | assertThat(environment.getRequiredProperty("dynamicParrot.addresses.1.zip")).isEqualTo("93118"); 340 | } 341 | 342 | @Test 343 | public void consumeTypedProperties() { 344 | assertThat(environment.getRequiredProperty("typedPigeon.name")).isEqualTo("Pigeon"); 345 | assertThat(environment.getRequiredProperty("typedPigeon.age")).isEqualTo("42"); 346 | assertThat(environment.getRequiredProperty("typedPigeon.addresses[0].street")) 347 | .isEqualTo("Wilmore St."); 348 | assertThat(environment.getRequiredProperty("typedPigeon.addresses[0].zip")).isEqualTo("94102"); 349 | 350 | assertThat(environment.getRequiredProperty("typedParrot.name")).isEqualTo("Parrot"); 351 | assertThat(environment.getRequiredProperty("typedParrot.age")).isEqualTo("21"); 352 | assertThat(environment.getRequiredProperty("typedParrot.addresses[1].street")) 353 | .isEqualTo("Leisure St."); 354 | assertThat(environment.getRequiredProperty("typedParrot.addresses[1].zip")).isEqualTo("93118"); 355 | } 356 | 357 | @Test 358 | public void consumeInt() { 359 | assertThat(appConfig.getIntProp()).isEqualTo(42L); 360 | assertThat(appConfig.getInt32()).isEqualTo(-42); 361 | } 362 | 363 | @Test 364 | public void consumeFloat() { 365 | assertThat(appConfig.getFloatProp()).isEqualTo(1.23d); 366 | } 367 | 368 | @Test 369 | public void consumeBoolean() { 370 | assertThat(appConfig.isBooleanProp()).isEqualTo(true); 371 | } 372 | 373 | @Test 374 | public void consumeString() { 375 | assertThat(appConfig.getString()).isEqualTo("string"); 376 | } 377 | 378 | @Test 379 | public void consumeDuration() { 380 | assertThat(appConfig.getDuration()).isEqualTo(new Duration(3, DurationUnit.HOURS)); 381 | } 382 | 383 | @Test 384 | public void consumeDataSize() { 385 | assertThat(appConfig.getDataSize()).isEqualTo(new DataSize(1.23, DataSizeUnit.GIGABYTES)); 386 | } 387 | 388 | @Test 389 | public void consumePair() { 390 | assertThat(appConfig.getPair()).isEqualTo(new Pair<>("hello", true)); 391 | } 392 | 393 | @Test 394 | public void consumeRegex() { 395 | assertThat(appConfig.getRegex().pattern()).isEqualTo("regex"); 396 | } 397 | 398 | @Test 399 | public void consumeNullableInt() { 400 | assertThat(appConfig.getNullableInt1()).isEqualTo(42L); 401 | assertThat(appConfig.getNullableInt2()).isNull(); 402 | } 403 | 404 | @Test 405 | public void consumeNullableString() { 406 | assertThat(appConfig.getNullableString1()).contains("string"); 407 | assertThat(appConfig.getNullableString2()).isNull(); 408 | } 409 | 410 | @Test 411 | @SuppressWarnings("ConstantConditions") 412 | public void consumeNullableTyped() { 413 | assertThat(appConfig.getNullablePigeon2()).isNull(); 414 | // doesn't work as desired; 415 | // apparently, Spring cannot both assemble an object from properties and lift that object to 416 | // `Optional` 417 | assertThat(appConfig.getNullablePigeon1()).isNotNull(); 418 | } 419 | 420 | @Test 421 | public void consumeListWithNullElements() { 422 | assertThat(appConfig.getListWithNullElements()).containsExactly("elem1", "elem2"); 423 | } 424 | 425 | @Test 426 | public void consumeSetWithNullElements() { 427 | assertThat(appConfig.getSetWithNullElements()).containsExactly("elem1", "elem2"); 428 | } 429 | 430 | @Test 431 | public void consumeMapWithNullElements() { 432 | assertThat(appConfig.getMapWithNullElements()) 433 | .containsExactly(entry(0L, "elem1"), entry(2L, "elem2")); 434 | } 435 | 436 | @Test 437 | public void consumeListingWithNullElements() { 438 | assertThat(appConfig.getListingWithNullElements()).containsExactly("elem1", "elem2"); 439 | } 440 | 441 | @Test 442 | public void consumeMappingWithNullElements() { 443 | assertThat(appConfig.getMappingWithNullElements()) 444 | .containsExactly(entry(0L, "elem1"), entry(2L, "elem2")); 445 | } 446 | 447 | @Test 448 | public void consumeEnum() { 449 | assertThat(appConfig.getSize()).isEqualTo(AppConfig.Size.SMALL); 450 | } 451 | 452 | @Test 453 | public void consumeList() { 454 | assertThat(appConfig.getSimpleList()).containsExactly("Pigeon", "Parrot"); 455 | assertThat(appConfig.getSimpleEmptyList()).isEmpty(); 456 | assertThat(appConfig.getComplexList()).containsExactly(pigeon, parrot); 457 | assertThat(appConfig.getComplexEmptyList()).isEmpty(); 458 | } 459 | 460 | @Test 461 | public void consumeSet() { 462 | assertThat(appConfig.getSimpleSet()).containsExactly("Pigeon", "Parrot"); 463 | assertThat(appConfig.getSimpleEmptySet()).isEmpty(); 464 | assertThat(appConfig.getComplexSet()).containsExactly(pigeon, parrot); 465 | assertThat(appConfig.getComplexEmptySet()).isEmpty(); 466 | } 467 | 468 | @Test 469 | public void consumeMap() { 470 | assertThat(appConfig.getSimpleMap()) 471 | .containsExactly(entry("Pigeon", 42L), entry("Parrot", 21L)); 472 | assertThat(appConfig.getSimpleEmptyMap()).isEmpty(); 473 | assertThat(appConfig.getComplexMap()) 474 | .containsExactly(entry("Pigeon", pigeon), entry("Parrot", parrot)); 475 | assertThat(appConfig.getComplexEmptyMap()).isEmpty(); 476 | } 477 | 478 | @Test 479 | public void consumeListing() { 480 | assertThat(appConfig.getSimpleListing()).containsExactly("Pigeon", "Parrot"); 481 | assertThat(appConfig.getSimpleEmptyListing()).isEmpty(); 482 | assertThat(appConfig.getComplexListing()).containsExactly(pigeon, parrot); 483 | assertThat(appConfig.getComplexEmptyListing()).isEmpty(); 484 | } 485 | 486 | @Test 487 | public void consumeMapping() { 488 | assertThat(appConfig.getSimpleMapping()) 489 | .containsExactly(entry("Pigeon", 42L), entry("Parrot", 21L)); 490 | assertThat(appConfig.getSimpleEmptyMapping()).isEmpty(); 491 | assertThat(appConfig.getComplexMapping()) 492 | .containsExactly(entry("Pigeon", pigeon), entry("Parrot", parrot)); 493 | assertThat(appConfig.getComplexEmptyMapping()).isEmpty(); 494 | } 495 | 496 | @Test 497 | @SuppressWarnings("ConstantConditions") 498 | public void consumeDynamic() { 499 | // not currently supported 500 | assertThat(appConfig.getDynamicPigeon()).isNull(); 501 | assertThat(appConfig.getDynamicParrot()).isNull(); 502 | } 503 | 504 | @Test 505 | public void consumeTyped() { 506 | var address1 = new AppConfig.Address("Wilmore St.", 94102); 507 | var address2 = new AppConfig.Address("Leisure St.", 93118); 508 | 509 | var pigeon = appConfig.getTypedPigeon(); 510 | assertThat(pigeon).isNotNull(); 511 | assertThat(pigeon.getName()).isEqualTo("Pigeon"); 512 | assertThat(pigeon.getAge()).isEqualTo(42L); 513 | assertThat(pigeon.getAddresses()).containsExactly(address1, address2); 514 | 515 | var parrot = appConfig.getTypedParrot(); 516 | assertThat(parrot).isNotNull(); 517 | assertThat(parrot.getName()).isEqualTo("Parrot"); 518 | assertThat(parrot.getAge()).isEqualTo(21L); 519 | assertThat(parrot.getAddresses()).containsExactly(address1, address2); 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /src/test/java/org/pkl/spring/boot/ConfigTestApp.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.pkl.spring.boot; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 21 | 22 | @SpringBootApplication(scanBasePackages = {"org.pkl.spring.boot"}) 23 | @EnableConfigurationProperties(AppConfig.class) 24 | public class ConfigTestApp { 25 | public static void main(final String... args) { 26 | new SpringApplication(ConfigTestApp.class).run(args); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/resources/application.pkl: -------------------------------------------------------------------------------- 1 | module org.pkl.spring.boot.AppConfig 2 | 3 | intProp: Int = 42 4 | 5 | int32: Int32 = -42 6 | 7 | floatProp: Float = 1.23 8 | 9 | booleanProp: Boolean = true 10 | 11 | string: String = "string" 12 | 13 | duration: Duration = 3.h 14 | 15 | dataSize: DataSize = 1.23.gb 16 | 17 | pair: Pair = Pair("hello", true) 18 | 19 | regex: Regex = Regex("regex") 20 | 21 | simpleList: List = List("Pigeon", "Parrot") 22 | 23 | simpleEmptyList: List = List() 24 | 25 | simpleSet: Set = Set("Pigeon", "Parrot") 26 | 27 | simpleEmptySet: Set = Set() 28 | 29 | simpleMap: Map = Map("Pigeon", 42, "Parrot", 21) 30 | 31 | simpleEmptyMap: Map = Map() 32 | 33 | simpleListing: Listing = new { "Pigeon"; "Parrot" } 34 | 35 | simpleEmptyListing: Listing = new {} 36 | 37 | simpleMapping: Mapping = new { ["Pigeon"] = 42; ["Parrot"] = 21 } 38 | 39 | simpleEmptyMapping: Mapping = new {} 40 | 41 | complexList: List = List( 42 | new Person { name = "Pigeon"; age = 42 }, 43 | new Person { name = "Parrot"; age = 21}) 44 | 45 | complexEmptyList: List = List() 46 | 47 | complexSet: Set = Set( 48 | new Person { name = "Pigeon"; age = 42 }, 49 | new Person { name = "Parrot"; age = 21}) 50 | 51 | complexEmptySet: Set = Set() 52 | 53 | complexMap: Map = Map( 54 | "Pigeon", new Person { name = "Pigeon"; age = 42 }, 55 | "Parrot", new Person { name = "Parrot"; age = 21}) 56 | 57 | complexEmptyMap: Map = Map() 58 | 59 | complexListing: Listing = new { 60 | new { name = "Pigeon"; age = 42 } 61 | new { name = "Parrot"; age = 21 } 62 | } 63 | 64 | complexEmptyListing: Listing = new {} 65 | 66 | complexMapping: Mapping = new { 67 | ["Pigeon"] { name = "Pigeon"; age = 42 } 68 | ["Parrot"] { name = "Parrot"; age = 21 } 69 | } 70 | 71 | complexEmptyMapping: Mapping = new {} 72 | 73 | dynamicPigeon: Dynamic = new { 74 | name = "Pigeon" 75 | age = 42 76 | addresses { 77 | new { 78 | street = "Wilmore St." 79 | zip = 94102 80 | } 81 | new { 82 | street = "Leisure St." 83 | zip = 93118 84 | } 85 | } 86 | } 87 | 88 | dynamicParrot: Dynamic = (dynamicPigeon) { 89 | name = "Parrot" 90 | age = 21 91 | } 92 | 93 | typedPigeon: Person = new { 94 | name = "Pigeon" 95 | age = 42 96 | addresses { 97 | new { 98 | street = "Wilmore St." 99 | zip = 94102 100 | } 101 | new { 102 | street = "Leisure St." 103 | zip = 93118 104 | } 105 | } 106 | } 107 | 108 | typedParrot: Person = (typedPigeon) { 109 | name = "Parrot" 110 | age = 21 111 | } 112 | 113 | nullableInt1: Int? = 42 114 | 115 | nullableInt2: Int? = null 116 | 117 | nullableString1: String? = "string" 118 | 119 | nullableString2: String? = null 120 | 121 | nullablePigeon1: Person? = typedPigeon 122 | 123 | nullablePigeon2: Person? = null 124 | 125 | listWithNullElements: List = List("elem1", "elem2") 126 | 127 | setWithNullElements: Set = Set("elem1", "elem2") 128 | 129 | mapWithNullElements: Map = Map(0, "elem1", 1, null, 2, "elem2", 3, null) 130 | 131 | listingWithNullElements: Listing = new { 132 | "elem1" 133 | "elem2" 134 | } 135 | 136 | mappingWithNullElements: Mapping = new { 137 | [0] = "elem1" 138 | [1] = null 139 | [2] = "elem2" 140 | [3] = null 141 | } 142 | 143 | size: Size = "SMALL" 144 | 145 | class Person { 146 | name: String 147 | age: Int 148 | addresses: Listing
149 | } 150 | 151 | class Address { 152 | street: String 153 | zip: Int 154 | } 155 | 156 | typealias Size = "SMALL"|"MEDIUM"|"LARGE" 157 | 158 | class EmptyObject 159 | --------------------------------------------------------------------------------