├── .github ├── .dependabot.yml ├── FUNDING.yml ├── README.md └── workflows │ ├── gradle.yml │ └── publish-to-reposilite.yml ├── .gitignore ├── .gitmodules ├── .run └── Examples __ Gradle - JavalinTest.run.xml ├── LICENSE ├── build.gradle.kts ├── examples ├── README.md ├── javalin-gradle-kotlin │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── compile │ │ └── openapi.groovy │ │ ├── java │ │ └── io │ │ │ └── javalin │ │ │ └── openapi │ │ │ └── plugin │ │ │ └── test │ │ │ └── JavalinTest.java │ │ ├── kotlin │ │ └── io │ │ │ └── javalin │ │ │ └── openapi │ │ │ └── plugin │ │ │ └── test │ │ │ └── KotlinEntity.kt │ │ └── resources │ │ ├── logback.xml │ │ └── tinylog.properties ├── javalin-maven-java │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── io │ │ │ └── javalin │ │ │ └── openapi │ │ │ └── plugin │ │ │ └── test │ │ │ └── JavalinTest.java │ │ ├── kotlin │ │ └── io │ │ │ └── javalin │ │ │ └── openapi │ │ │ └── plugin │ │ │ └── test │ │ │ └── KotlinEntity.kt │ │ └── resources │ │ └── logback.xml └── javalin-maven-kotlin │ ├── .mvn │ └── jvm.config │ ├── README.md │ ├── pom.xml │ └── src │ └── main │ ├── kotlin │ └── io │ │ └── javalin │ │ └── openapi │ │ └── plugin │ │ └── test │ │ └── KotlinTest.kt │ └── resources │ └── logback.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── javalin-plugins ├── javalin-openapi-plugin │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── kotlin │ │ │ └── io │ │ │ └── javalin │ │ │ └── openapi │ │ │ └── plugin │ │ │ ├── OpenApiConfiguration.kt │ │ │ ├── OpenApiHandler.kt │ │ │ └── OpenApiPlugin.kt │ │ └── test │ │ └── kotlin │ │ └── OpenApiPluginTest.kt ├── javalin-redoc-plugin │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── kotlin │ │ │ └── io │ │ │ └── javalin │ │ │ └── openapi │ │ │ └── plugin │ │ │ └── redoc │ │ │ ├── ReDocHandler.kt │ │ │ ├── ReDocPlugin.kt │ │ │ └── ReDocWebJarHandler.kt │ │ └── test │ │ ├── kotlin │ │ └── io │ │ │ └── javalin │ │ │ └── openapi │ │ │ └── plugin │ │ │ └── redoc │ │ │ ├── RedocPluginTest.kt │ │ │ └── specification │ │ │ └── JavalinBehindProxy.kt │ │ └── resources │ │ ├── logback.xml │ │ └── openapi-plugin │ │ ├── .index │ │ └── test.json └── javalin-swagger-plugin │ ├── build.gradle.kts │ └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── javalin │ │ └── openapi │ │ └── plugin │ │ └── swagger │ │ ├── SwaggerHandler.kt │ │ ├── SwaggerPlugin.kt │ │ └── SwaggerWebJarHandler.kt │ └── test │ ├── kotlin │ └── io │ │ └── javalin │ │ └── openapi │ │ └── plugin │ │ └── swagger │ │ ├── SwaggerPluginTest.kt │ │ └── specification │ │ └── JavalinBehindProxy.kt │ └── resources │ ├── logback.xml │ └── openapi-plugin │ ├── .index │ └── test.json ├── openapi-annotation-processor ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── io │ │ │ └── javalin │ │ │ └── openapi │ │ │ └── processor │ │ │ ├── AnnotationProcessorTools.kt │ │ │ ├── OpenApiAnnotationProcessor.kt │ │ │ ├── configuration │ │ │ └── OpenApiPrecompileScriptingEngine.kt │ │ │ └── generators │ │ │ ├── JsonSchemaGenerator.kt │ │ │ └── OpenApiGenerator.kt │ └── resources │ │ ├── META-INF │ │ └── services │ │ │ └── javax.annotation.processing.Processor │ │ └── logback.xml │ └── test │ ├── compile │ └── openapi.groovy │ ├── kotlin │ └── io │ │ └── javalin │ │ └── openapi │ │ └── processor │ │ ├── ComponentAnnotationsTest.kt │ │ ├── CompositionTest.kt │ │ ├── CustomAnnotationsTest.kt │ │ ├── CustomTypeMappingsTest.kt │ │ ├── OpenApiAnnotationTest.kt │ │ ├── SchemeTest.kt │ │ ├── TypeMappersTest.kt │ │ ├── UserCasesTest.kt │ │ └── specification │ │ └── OpenApiAnnotationProcessorSpecification.kt │ └── resources │ ├── META-INF │ └── services │ │ └── javax.annotation.processing.Processor │ └── logback-test.xml ├── openapi-specification ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── javalin │ └── openapi │ ├── Info.kt │ ├── JsonSchemaAnnotations.kt │ ├── OpenApiAnnotations.kt │ ├── Security.kt │ ├── Server.kt │ ├── data │ └── OpenApiAnnotationsData.kt │ └── experimental │ ├── AnnotationProcessorContext.kt │ ├── ClassDefinitionApi.kt │ ├── OpenApiAnnotationProcessorConfiguration.kt │ ├── OpenApiAnnotationProcessorParameters.kt │ ├── defaults │ ├── ArrayEmbeddedTypeProcessor.kt │ ├── CompositionEmbeddedTypeProcessor.kt │ ├── DefaultSimpleTypeMappings.kt │ └── DictionaryEmbeddedTypeProcessor.kt │ └── processor │ ├── generators │ ├── CompositionGenerator.kt │ ├── ExampleGenerator.kt │ └── TypeSchemaGenerator.kt │ └── shared │ ├── AnnotationProcessorExtensions.kt │ ├── JsonExtensions.kt │ └── ModelExtensions.kt └── settings.gradle.kts /.github/.dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: dzikoysk 2 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Plugin [![CI](https://github.com/javalin/javalin-openapi/actions/workflows/gradle.yml/badge.svg)](https://github.com/javalin/javalin-openapi/actions/workflows/gradle.yml) ![Maven Central](https://img.shields.io/maven-central/v/io.javalin.community.openapi/openapi-annotation-processor?label=Maven%20Central) [![Version / Snapshot](https://maven.reposilite.com/api/badge/latest/snapshots/io/javalin/community/openapi/javalin-openapi-plugin?color=A97BFF&name=Snapshot)](https://maven.reposilite.com/#/snapshots/io/javalin/community/openapi) 2 | Compile-time OpenAPI integration for Javalin 6.x ecosystem. 3 | This is a new plugin that replaces [old built-in OpenApi module](https://github.com/javalin/javalin/tree/javalin-4x/javalin-openapi), 4 | the API looks quite the same despite some minor changes. 5 | 6 | ![Preview](https://user-images.githubusercontent.com/4235722/122982162-d2344f80-d39a-11eb-9a93-e52b9b7b7b53.png) 7 | 8 | ### How to use 9 | 10 | * [Wiki / Installation](https://github.com/javalin/javalin-openapi/wiki/1.-Installation) 11 | * [Wiki / Setup](https://github.com/javalin/javalin-openapi/wiki/2.-Setup) 12 | * [Wiki / Features](https://github.com/javalin/javalin-openapi/wiki/3.-Features) 13 | 14 | ### Notes 15 | * Reflection free, does not perform any extra operations at runtime 16 | * Uses `@OpenApi` to simplify migration from bundled OpenApi implementation 17 | * Supports Java 11+ (also 16 and any further releases) and Kotlin (through [Kapt](https://kotlinlang.org/docs/kapt.html)) 18 | * Uses internal WebJar handler that works with `/*` route out of the box 19 | * Provides better projection of OpenAPI specification 20 | * Schema validation through Swagger core module 21 | 22 | ### Other examples 23 | * [Test module](https://github.com/javalin/javalin-openapi/blob/main/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java) - `JavalinTest` shows how this plugin work in Java codebase using various features 24 | * [Reposilite](https://github.com/dzikoysk/reposilite) - real world app using Javalin and OpenApi integration 25 | 26 | ### Repository structure 27 | 28 | #### Universal modules 29 | 30 | | Module | Description | 31 | |:-------------------------------|:-------------------------------------------------------------------------------------------| 32 | | `openapi-annotation-processor` | Compile-time annotation processor, should generate `/openapi-plugin/openapi.json` resource | 33 | | `openapi-specification` | Annotations & classes used to describe OpenAPI specification | 34 | | `openapi-test` | Example Javalin application that uses OpenApi plugin in Gradle & Maven | 35 | 36 | #### Javalin plugins 37 | 38 | | Plugin | Description | 39 | |:-------------------------|:-------------------------------------------------------------------------------| 40 | | `javalin-openapi-plugin` | Loads `/openapi-plugin/openapi.json` resource and serves main OpenApi endpoint | 41 | | `javalin-swagger-plugin` | Serves Swagger UI | 42 | | `javalin-redoc-plugin` | Serves ReDoc UI | 43 | 44 | #### Branches 45 | 46 | | Branch | Javalin version | OpenApi Version | Java Version | 47 | |:-------------------------------------------------------------|:----------------|:----------------|:-------------| 48 | | [main](https://github.com/javalin/javalin-openapi/tree/main) | 6.x | 6.x | JDK11 | 49 | | [5.x](https://github.com/javalin/javalin-openapi/tree/5.x) | 5.x | 5.x | JDK11 | 50 | | [4.x](https://github.com/javalin/javalin-openapi/tree/4.x) | 4.x | 1.x | JDK8 | 51 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | build: 9 | name: "Build with JDK${{ matrix.jdk }}" 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | jdk: [ 17, 21 ] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: ${{ matrix.jdk }} 20 | - name: Grant execute permission for gradlew 21 | run: chmod +x gradlew 22 | - name: Build with Gradle 23 | run: ./gradlew build 24 | - name: Upload coverage to Codecov 25 | uses: codecov/codecov-action@v1 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-reposilite.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow to automatically publish snapshot builds. 2 | name: "Publish snapshots" 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | env: 8 | JAVA_VERSION: 17 9 | 10 | jobs: 11 | maven: 12 | name: "Maven" 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: "Checkout repository" 16 | uses: actions/checkout@v3 17 | 18 | - name: "Set up Java ${{ env.JAVA_VERSION }}" 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: "${{ env.JAVA_VERSION }}" 22 | distribution: "adopt" 23 | 24 | - name: "Grant execute permission for gradlew" 25 | run: chmod +x gradlew 26 | 27 | - name: "Gradle publish" 28 | uses: gradle/gradle-build-action@v2 29 | with: 30 | arguments: "clean build publishAllPublicationsToReposilite-repositoryRepository" 31 | env: 32 | MAVEN_NAME: ${{ secrets.MAVEN_NAME }} 33 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/gradle,java,maven,intellij 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=gradle,java,maven,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/ 11 | 12 | # Gradle and Maven with auto-import 13 | # When using Gradle or Maven with auto-import, you should exclude module files, 14 | # since they will be recreated, and may cause churn. Uncomment if using 15 | # auto-import. 16 | # .idea/artifacts 17 | # .idea/compiler.xml 18 | # .idea/jarRepositories.xml 19 | # .idea/modules.xml 20 | # .idea/*.iml 21 | # .idea/modules 22 | # *.iml 23 | # *.ipr 24 | 25 | # CMake 26 | cmake-build-*/ 27 | 28 | # Mongo Explorer plugin 29 | .idea/**/mongoSettings.xml 30 | 31 | # File-based project format 32 | *.iws 33 | *.iml 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | 53 | # Editor-based Rest Client 54 | .idea/httpRequests 55 | 56 | # Android studio 3.1+ serialized cache file 57 | .idea/caches/build_file_checksums.ser 58 | 59 | ### Intellij Patch ### 60 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 61 | 62 | # *.iml 63 | # modules.xml 64 | # .idea/misc.xml 65 | # *.ipr 66 | 67 | # Sonarlint plugin 68 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 69 | .idea/**/sonarlint/ 70 | 71 | # SonarQube Plugin 72 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 73 | .idea/**/sonarIssues.xml 74 | 75 | # Markdown Navigator plugin 76 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 77 | .idea/**/markdown-navigator.xml 78 | .idea/**/markdown-navigator-enh.xml 79 | .idea/**/markdown-navigator/ 80 | 81 | # Cache file creation bug 82 | # See https://youtrack.jetbrains.com/issue/JBR-2257 83 | .idea/$CACHE_FILE$ 84 | 85 | # CodeStream plugin 86 | # https://plugins.jetbrains.com/plugin/12206-codestream 87 | .idea/codestream.xml 88 | 89 | ### Java ### 90 | # Compiled class file 91 | *.class 92 | 93 | # Log file 94 | *.log 95 | 96 | # BlueJ files 97 | *.ctxt 98 | 99 | # Mobile Tools for Java (J2ME) 100 | .mtj.tmp/ 101 | 102 | # Package Files # 103 | *.jar 104 | *.war 105 | *.nar 106 | *.ear 107 | *.zip 108 | *.tar.gz 109 | *.rar 110 | 111 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 112 | hs_err_pid* 113 | 114 | ### Maven ### 115 | target/ 116 | pom.xml.tag 117 | pom.xml.releaseBackup 118 | pom.xml.versionsBackup 119 | pom.xml.next 120 | release.properties 121 | dependency-reduced-pom.xml 122 | buildNumber.properties 123 | .mvn/timing.properties 124 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 125 | .mvn/wrapper/maven-wrapper.jar 126 | .flattened-pom.xml 127 | 128 | ### Gradle ### 129 | .gradle 130 | build/ 131 | 132 | # Ignore Gradle GUI config 133 | gradle-app.setting 134 | 135 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 136 | !gradle-wrapper.jar 137 | 138 | # Cache of project 139 | .gradletasknamecache 140 | 141 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 142 | # gradle/wrapper/gradle-wrapper.properties 143 | 144 | ### Gradle Patch ### 145 | **/build/ 146 | 147 | # End of https://www.toptal.com/developers/gitignore/api/gradle,java,maven,intellij 148 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wiki"] 2 | path = wiki 3 | url = https://github.com/javalin/javalin-openapi.wiki.git 4 | -------------------------------------------------------------------------------- /.run/Examples __ Gradle - JavalinTest.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | `java-library` 5 | kotlin("jvm") version "1.9.22" 6 | `maven-publish` 7 | signing 8 | id("io.github.gradle-nexus.publish-plugin") version "1.3.0" 9 | } 10 | 11 | description = "Javalin OpenAPI Parent | Parent" 12 | 13 | allprojects { 14 | apply(plugin = "java-library") 15 | apply(plugin = "signing") 16 | apply(plugin = "maven-publish") 17 | 18 | group = "io.javalin.community.openapi" 19 | version = "6.6.0" 20 | 21 | repositories { 22 | mavenCentral() 23 | maven("https://maven.reposilite.com/snapshots") 24 | } 25 | 26 | publishing { 27 | repositories { 28 | maven { 29 | name = "reposilite-repository" 30 | url = uri("https://maven.reposilite.com/${if (version.toString().endsWith("-SNAPSHOT")) "snapshots" else "releases"}") 31 | 32 | credentials { 33 | username = getEnvOrProperty("MAVEN_NAME", "mavenUser") 34 | password = getEnvOrProperty("MAVEN_TOKEN", "mavenPassword") 35 | } 36 | } 37 | } 38 | } 39 | 40 | afterEvaluate { 41 | description 42 | ?.takeIf { it.isNotEmpty() } 43 | ?.split("|") 44 | ?.let { (projectName, projectDescription) -> 45 | publishing { 46 | publications { 47 | create("library") { 48 | pom { 49 | name.set(projectName) 50 | description.set(projectDescription) 51 | url.set("https://github.com/javalin/javalin-openapi") 52 | 53 | licenses { 54 | license { 55 | name.set("The Apache License, Version 2.0") 56 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") 57 | } 58 | } 59 | developers { 60 | developer { 61 | id.set("dzikoysk") 62 | name.set("dzikoysk") 63 | email.set("dzikoysk@dzikoysk.net") 64 | } 65 | } 66 | scm { 67 | connection.set("scm:git:git://github.com/javalin/javalin-openapi.git") 68 | developerConnection.set("scm:git:ssh://github.com/javalin/javalin-openapi.git") 69 | url.set("https://github.com/javalin/javalin-openapi.git") 70 | } 71 | } 72 | 73 | from(components.getByName("java")) 74 | } 75 | } 76 | } 77 | 78 | if (findProperty("signing.keyId").takeIf { it != null && it.toString().trim().isNotEmpty() } != null) { 79 | signing { 80 | sign(publishing.publications.getByName("library")) 81 | } 82 | } 83 | } 84 | } 85 | 86 | java { 87 | withJavadocJar() 88 | withSourcesJar() 89 | } 90 | 91 | java { 92 | sourceCompatibility = JavaVersion.VERSION_11 93 | targetCompatibility = JavaVersion.VERSION_11 94 | } 95 | 96 | tasks.withType().configureEach { 97 | kotlinOptions { 98 | jvmTarget = "11" 99 | languageVersion = "1.8" 100 | freeCompilerArgs = listOf( 101 | "-Xjvm-default=all", // For generating default methods in interfaces 102 | // "-Xcontext-receivers" 103 | ) 104 | } 105 | } 106 | } 107 | 108 | subprojects { 109 | apply(plugin = "application") 110 | apply(plugin = "org.jetbrains.kotlin.jvm") 111 | 112 | dependencies { 113 | val javalin = "6.6.0" 114 | compileOnly("io.javalin:javalin:$javalin") 115 | testImplementation("io.javalin:javalin:$javalin") 116 | 117 | val junit = "5.9.3" 118 | testImplementation("org.junit.jupiter:junit-jupiter-params:$junit") 119 | testImplementation("org.junit.jupiter:junit-jupiter-api:$junit") 120 | testImplementation("org.junit.jupiter:junit-jupiter-engine:$junit") 121 | 122 | testImplementation("org.assertj:assertj-core:3.24.2") 123 | testImplementation("net.javacrumbs.json-unit:json-unit-assertj:2.38.0") 124 | testImplementation("com.konghq:unirest-java:3.14.2") 125 | 126 | testImplementation("ch.qos.logback:logback-classic:1.4.14") 127 | } 128 | 129 | tasks.withType { 130 | useJUnitPlatform() 131 | } 132 | } 133 | 134 | nexusPublishing { 135 | repositories { 136 | sonatype { 137 | username.set(getEnvOrProperty("SONATYPE_USER", "sonatypeUser")) 138 | password.set(getEnvOrProperty("SONATYPE_PASSWORD", "sonatypePassword")) 139 | } 140 | } 141 | } 142 | 143 | fun getEnvOrProperty(env: String, property: String): String? = 144 | System.getenv(env) ?: findProperty(property)?.toString() 145 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | -------------------------------------------------------------------------------- /examples/javalin-gradle-kotlin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("kapt") 5 | } 6 | 7 | java { 8 | sourceCompatibility = JavaVersion.VERSION_17 9 | targetCompatibility = JavaVersion.VERSION_17 10 | } 11 | 12 | tasks.withType().configureEach { 13 | kotlinOptions { 14 | jvmTarget = "17" 15 | languageVersion = "1.8" 16 | freeCompilerArgs = listOf("-Xjvm-default=all") // For generating default methods in interfaces 17 | } 18 | } 19 | 20 | sourceSets.getByName("main") { 21 | java.srcDir("src/main/kotlin") 22 | } 23 | 24 | dependencies { 25 | // declare lombok annotation processor as first 26 | val lombok = "1.18.28" 27 | compileOnly("org.projectlombok:lombok:$lombok") 28 | annotationProcessor("org.projectlombok:lombok:$lombok") 29 | testCompileOnly("org.projectlombok:lombok:$lombok") 30 | testAnnotationProcessor("org.projectlombok:lombok:$lombok") 31 | implementation("jakarta.validation:jakarta.validation-api:2.0.2") 32 | 33 | // then openapi annotation processor 34 | kapt(project(":openapi-annotation-processor")) 35 | implementation(project(":javalin-plugins:javalin-openapi-plugin")) 36 | implementation(project(":javalin-plugins:javalin-swagger-plugin")) 37 | implementation(project(":javalin-plugins:javalin-redoc-plugin")) 38 | testImplementation("org.apache.groovy:groovy:4.0.12") 39 | 40 | // javalin 41 | implementation("io.javalin:javalin:6.6.0") 42 | implementation("com.fasterxml.jackson.core:jackson-databind:2.18.1") 43 | 44 | // logging 45 | implementation("ch.qos.logback:logback-classic:1.4.14") 46 | 47 | // some test integrations 48 | implementation("org.mongodb:bson:4.9.1") 49 | } 50 | 51 | kapt { 52 | arguments { 53 | arg("openapi.info.title", "Awesome App") 54 | arg("openapi.info.version", "1.0.0") 55 | } 56 | } 57 | 58 | repositories { 59 | mavenCentral() 60 | } 61 | -------------------------------------------------------------------------------- /examples/javalin-gradle-kotlin/src/main/compile/openapi.groovy: -------------------------------------------------------------------------------- 1 | import io.javalin.openapi.experimental.ExperimentalCompileOpenApiConfiguration 2 | import io.javalin.openapi.experimental.OpenApiAnnotationProcessorConfiguration 3 | import io.javalin.openapi.experimental.OpenApiAnnotationProcessorConfigurer 4 | 5 | @ExperimentalCompileOpenApiConfiguration 6 | class OpenApiConfiguration implements OpenApiAnnotationProcessorConfigurer { 7 | 8 | @Override 9 | void configure(OpenApiAnnotationProcessorConfiguration openApiAnnotationProcessorConfiguration) { 10 | // openApiAnnotationProcessorConfiguration.debug = true 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /examples/javalin-gradle-kotlin/src/main/kotlin/io/javalin/openapi/plugin/test/KotlinEntity.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.test 2 | 3 | import io.javalin.openapi.CustomAnnotation 4 | import io.javalin.openapi.JsonSchema 5 | import io.javalin.openapi.OneOf 6 | import io.javalin.openapi.plugin.test.JavalinTest.Description 7 | 8 | @JsonSchema( 9 | generateResource = false, 10 | requireNonNulls = false 11 | ) 12 | data class KotlinEntity( 13 | val name: String, 14 | val primitive: Int, 15 | val custom: Elements, 16 | val oneOfResult: Result, 17 | val nullable: Any?, 18 | ) 19 | 20 | @JsonSchema( 21 | requireNonNulls = false 22 | ) 23 | @Description( 24 | title = "Kotlin Scheme", 25 | description = 26 | """ 27 | Example usage of custom annotation on Kotlin class 28 | """, 29 | statusCode = -1 30 | ) 31 | data class KotlinScheme( 32 | @get:Description(title = "Value", description = "Int value", statusCode = 200) 33 | val value: Int, 34 | @get:OneOf(KotlinEntity::class) 35 | val any: Any, 36 | ) 37 | 38 | @CustomAnnotation 39 | @Target(AnnotationTarget.PROPERTY_GETTER) 40 | annotation class CustomAnnotationInKotlinWithArray( 41 | val standard: String = "default", 42 | // should support arrays 43 | val value: Array = [], 44 | ) 45 | 46 | @JsonSchema 47 | data class Elements( 48 | @get:CustomAnnotationInKotlinWithArray(value = ["a", "b"]) 49 | val value: String 50 | ) 51 | 52 | @OneOf(Ok::class, Error::class) 53 | sealed interface Result 54 | object Ok : Result 55 | object Error : Result -------------------------------------------------------------------------------- /examples/javalin-gradle-kotlin/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/javalin-gradle-kotlin/src/main/resources/tinylog.properties: -------------------------------------------------------------------------------- 1 | writer = console 2 | writer.level = info 3 | writer.format = {date: HH:mm:ss.SSS} {level} | {message} -------------------------------------------------------------------------------- /examples/javalin-maven-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.javalin.community.openapi.examples 8 | javalin-apptest 9 | 1.0.0 10 | 11 | 12 | 11 13 | 11 14 | 6.6.0 15 | 6.6.0 16 | 17 | 18 | 19 | 20 | reposilite-repository 21 | https://maven.reposilite.com/snapshots 22 | 23 | 24 | 25 | 26 | 27 | io.javalin 28 | javalin 29 | ${javalin.version} 30 | 31 | 32 | io.javalin.community.openapi 33 | javalin-openapi-plugin 34 | ${javalin.openapi.version} 35 | 36 | 37 | io.javalin.community.openapi 38 | javalin-swagger-plugin 39 | ${javalin.openapi.version} 40 | 41 | 42 | io.javalin.community.openapi 43 | javalin-redoc-plugin 44 | ${javalin.openapi.version} 45 | 46 | 47 | org.webjars.npm 48 | redoc 49 | 2.0.0-rc.56 50 | 51 | 52 | * 53 | * 54 | 55 | 56 | 57 | 58 | org.tinylog 59 | tinylog-api 60 | 2.4.1 61 | 62 | 63 | org.tinylog 64 | tinylog-impl 65 | 2.4.1 66 | 67 | 68 | org.tinylog 69 | slf4j-tinylog 70 | 2.4.1 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-compiler-plugin 80 | 3.10.1 81 | 82 | 83 | 84 | io.javalin.community.openapi 85 | openapi-annotation-processor 86 | ${javalin.openapi.version} 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /examples/javalin-maven-java/src/main/kotlin/io/javalin/openapi/plugin/test/KotlinEntity.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.test 2 | 3 | data class KotlinEntity( 4 | val name: String, 5 | val value: Int 6 | ) -------------------------------------------------------------------------------- /examples/javalin-maven-java/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/javalin-maven-kotlin/.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | --add-opens=java.base/java.lang=ALL-UNNAMED 2 | --add-opens=java.base/java.io=ALL-UNNAMED -------------------------------------------------------------------------------- /examples/javalin-maven-kotlin/README.md: -------------------------------------------------------------------------------- 1 | # javalin-maven-kotlin 2 | 3 | This is a simple example of a Javalin application using Maven and Kotlin. 4 | In order to generate OpenAPI specification with Maven, run the following command: 5 | 6 | ```shell 7 | $ mvn clean compile 8 | ``` 9 | 10 | Once the command is executed, the OpenAPI annotation processor will generate output files in the `target/classes/openapi-plugin` directory. 11 | These files will be picked up by the OpenAPI plugin and hosted at `http://localhost:8080/openapi`. 12 | You can also access the Swagger UI at `http://localhost:8080/swagger-ui`. 13 | -------------------------------------------------------------------------------- /examples/javalin-maven-kotlin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.javalin.community.openapi.examples 8 | javalin-apptest 9 | 1.0.0 10 | 11 | 12 | 6.6.0 13 | 6.6.0 14 | 2.1.0 15 | 16 | 17 | 18 | 19 | reposilite-repository 20 | https://maven.reposilite.com/snapshots 21 | 22 | 23 | 24 | 25 | 26 | org.jetbrains.kotlin 27 | kotlin-stdlib 28 | ${kotlin.version} 29 | 30 | 31 | 32 | io.javalin 33 | javalin 34 | ${javalin.version} 35 | 36 | 37 | io.javalin.community.openapi 38 | javalin-openapi-plugin 39 | ${javalin.openapi.version} 40 | 41 | 42 | io.javalin.community.openapi 43 | javalin-swagger-plugin 44 | ${javalin.openapi.version} 45 | 46 | 47 | io.javalin.community.openapi 48 | javalin-redoc-plugin 49 | ${javalin.openapi.version} 50 | 51 | 52 | org.webjars.npm 53 | redoc 54 | 2.0.0-rc.56 55 | 56 | 57 | * 58 | * 59 | 60 | 61 | 62 | 63 | 64 | org.tinylog 65 | tinylog-api 66 | 2.4.1 67 | 68 | 69 | org.tinylog 70 | tinylog-impl 71 | 2.4.1 72 | 73 | 74 | org.tinylog 75 | slf4j-tinylog 76 | 2.4.1 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | kotlin-maven-plugin 85 | org.jetbrains.kotlin 86 | 2.1.0 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.jetbrains.kotlin 94 | kotlin-maven-plugin 95 | ${kotlin.version} 96 | 97 | 98 | kapt 99 | 100 | kapt 101 | 102 | 103 | 104 | src/main/kotlin 105 | 106 | 107 | 108 | io.javalin.community.openapi 109 | openapi-annotation-processor 110 | ${javalin.openapi.version} 111 | 112 | 113 | 114 | 115 | 116 | compile 117 | compile 118 | 119 | compile 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /examples/javalin-maven-kotlin/src/main/kotlin/io/javalin/openapi/plugin/test/KotlinTest.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.test 2 | 3 | import io.javalin.Javalin 4 | import io.javalin.openapi.HttpMethod 5 | import io.javalin.openapi.OpenApi 6 | import io.javalin.openapi.plugin.OpenApiPlugin 7 | import io.javalin.openapi.plugin.swagger.SwaggerPlugin 8 | 9 | @OpenApi( 10 | description = "Test description", 11 | summary = "Test summary", 12 | tags = ["test-tag"], 13 | methods = [HttpMethod.GET], 14 | path = "/" 15 | ) 16 | fun main() { 17 | Javalin.createAndStart { config -> 18 | config.registerPlugin( 19 | OpenApiPlugin { 20 | it.documentationPath = "/openapi" 21 | } 22 | ) 23 | 24 | config.registerPlugin( 25 | SwaggerPlugin { 26 | it.uiPath = "/swagger" 27 | it.documentationPath = "/openapi" 28 | } 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /examples/javalin-maven-kotlin/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle 2 | org.gradle.daemon=false 3 | org.gradle.parallel=false 4 | org.gradle.jvmargs=-Dfile.encoding=UTF-8 5 | org.gradle.logging.stacktrace=full 6 | 7 | # Kapt 8 | kapt.include.compile.classpath=false 9 | # kapt.incremental.apt=false 10 | # kapt.use.worker.api=false 11 | 12 | # Maven 13 | mavenUser= 14 | mavenPassword= 15 | 16 | # Sonatype 17 | sonatypeUser= 18 | sonatypePassword= 19 | 20 | # Sign 21 | signing.keyId= 22 | signing.password= 23 | signing.secretKeyRingFile= 24 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javalin/javalin-openapi/5ae46dabcd883fc1f5ef40a3b832bf578cc0349b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /javalin-plugins/javalin-openapi-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Javalin OpenAPI Plugin | Serve raw OpenApi documentation under dedicated endpoint" 2 | 3 | plugins { 4 | kotlin("kapt") 5 | } 6 | 7 | dependencies { 8 | api(project(":openapi-specification")) 9 | 10 | kaptTest(project(":openapi-annotation-processor")) 11 | } 12 | -------------------------------------------------------------------------------- /javalin-plugins/javalin-openapi-plugin/src/main/kotlin/io/javalin/openapi/plugin/OpenApiConfiguration.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MemberVisibilityCanBePrivate") 2 | 3 | package io.javalin.openapi.plugin 4 | 5 | import com.fasterxml.jackson.databind.node.ObjectNode 6 | import io.javalin.openapi.ApiKeyAuth 7 | import io.javalin.openapi.BasicAuth 8 | import io.javalin.openapi.BearerAuth 9 | import io.javalin.openapi.CookieAuth 10 | import io.javalin.openapi.OAuth2 11 | import io.javalin.openapi.OpenApiInfo 12 | import io.javalin.openapi.OpenApiServer 13 | import io.javalin.openapi.OpenID 14 | import io.javalin.openapi.Security 15 | import io.javalin.openapi.SecurityScheme 16 | import io.javalin.security.RouteRole 17 | import java.util.function.BiConsumer 18 | import java.util.function.Consumer 19 | 20 | /** Configure OpenApi plugin */ 21 | class OpenApiPluginConfiguration @JvmOverloads constructor( 22 | @JvmField var documentationPath: String = "/openapi", 23 | @JvmField var roles: List? = null, 24 | @JvmField var prettyOutputEnabled: Boolean = true, 25 | @JvmField var definitionConfiguration: BiConsumer? = null 26 | ) { 27 | 28 | /** Path to host documentation as JSON */ 29 | fun withDocumentationPath(path: String): OpenApiPluginConfiguration = also { 30 | this.documentationPath = path 31 | } 32 | 33 | /** List of roles eligible to access OpenApi routes */ 34 | fun withRoles(vararg roles: RouteRole): OpenApiPluginConfiguration = also { 35 | this.roles = roles.toList() 36 | } 37 | 38 | /** Path to host documentation as JSON */ 39 | @JvmOverloads 40 | fun withPrettyOutput(enabled: Boolean = true): OpenApiPluginConfiguration = also { 41 | this.prettyOutputEnabled = enabled 42 | } 43 | 44 | /* Dynamically apply custom changes to generated OpenApi specifications */ 45 | fun withDefinitionConfiguration(definitionConfigurationConfigurer: BiConsumer): OpenApiPluginConfiguration = also { 46 | definitionConfiguration = definitionConfigurationConfigurer 47 | } 48 | 49 | } 50 | 51 | /** Modify OpenApi documentation represented by [ObjectNode] in JSON format */ 52 | fun interface DefinitionProcessor { 53 | fun process(content: ObjectNode): String 54 | } 55 | 56 | class DefinitionConfiguration @JvmOverloads constructor( 57 | @JvmField @JvmSynthetic internal var info: OpenApiInfo? = null, 58 | @JvmField @JvmSynthetic internal var servers: MutableList = mutableListOf(), 59 | @JvmField @JvmSynthetic internal var security: SecurityComponentConfiguration? = null, 60 | @JvmField @JvmSynthetic internal var definitionProcessor: DefinitionProcessor? = null 61 | ) { 62 | 63 | /** Define custom info object */ 64 | fun withInfo(openApiInfo: Consumer): DefinitionConfiguration = also { 65 | this.info = OpenApiInfo().also { openApiInfo.accept(it) } 66 | } 67 | 68 | @Deprecated("Use withInfo instead", ReplaceWith("withInfo(openApiInfo)")) 69 | fun withOpenApiInfo(openApiInfo: Consumer): DefinitionConfiguration = 70 | withInfo(openApiInfo) 71 | 72 | /** Add custom server **/ 73 | fun withServer(server: OpenApiServer): DefinitionConfiguration = also { 74 | this.servers.add(server) 75 | } 76 | 77 | /** Add custom server **/ 78 | fun withServer(serverConfigurer: Consumer): DefinitionConfiguration = also { 79 | this.servers.add(OpenApiServer().also { serverConfigurer.accept(it) }) 80 | } 81 | 82 | /** Define custom security object */ 83 | fun withSecurity(securityConfigurer: Consumer): DefinitionConfiguration = also { 84 | SecurityComponentConfiguration() 85 | .also { securityConfigurer.accept(it) } 86 | .let { withSecurity(it) } 87 | } 88 | 89 | /** Define custom security object */ 90 | fun withSecurity(securityConfiguration: SecurityComponentConfiguration): DefinitionConfiguration = also { 91 | this.security = securityConfiguration 92 | } 93 | 94 | /** Register scheme processor */ 95 | fun withDefinitionProcessor(definitionProcessor: DefinitionProcessor): DefinitionConfiguration = also { 96 | this.definitionProcessor = definitionProcessor 97 | } 98 | 99 | } 100 | 101 | class SecurityComponentConfiguration @JvmOverloads constructor( 102 | @JvmField @JvmSynthetic internal val securitySchemes: MutableMap = mutableMapOf(), 103 | @JvmField @JvmSynthetic internal val globalSecurity: MutableList = mutableListOf() 104 | ) { 105 | 106 | fun withSecurityScheme(schemeName: String, securityScheme: SecurityScheme): SecurityComponentConfiguration = also { 107 | securitySchemes[schemeName] = securityScheme 108 | } 109 | 110 | @JvmOverloads 111 | fun withBasicAuth(schemeName: String = "BasicAuth", securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = 112 | withSecurityScheme(schemeName, BasicAuth().also { securityScheme.accept(it) }) 113 | 114 | @JvmOverloads 115 | fun withBearerAuth(schemeName: String = "BearerAuth", securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = 116 | withSecurityScheme(schemeName, BearerAuth().also { securityScheme.accept(it) }) 117 | 118 | @JvmOverloads 119 | fun withApiKeyAuth(schemeName: String = "ApiKeyAuth", apiKeyHeader: String = "X-Api-Key", securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = 120 | withSecurityScheme(schemeName, ApiKeyAuth(name = apiKeyHeader).also { securityScheme.accept(it) }) 121 | 122 | @JvmOverloads 123 | fun withCookieAuth(schemeName: String = "CookieAuth", sessionCookie: String = "JSESSIONID", securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = 124 | withSecurityScheme(schemeName, CookieAuth(name = sessionCookie).also { securityScheme.accept(it) }) 125 | 126 | @JvmOverloads 127 | fun withOpenID(schemeName: String, openIdConnectUrl: String, securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = 128 | withSecurityScheme(schemeName, OpenID(openIdConnectUrl = openIdConnectUrl).also { securityScheme.accept(it) }) 129 | 130 | @JvmOverloads 131 | fun withOAuth2(schemeName: String, description: String, securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = 132 | withSecurityScheme(schemeName, OAuth2(description = description).also { securityScheme.accept(it) }) 133 | 134 | fun withGlobalSecurity(security: Security): SecurityComponentConfiguration = also { 135 | globalSecurity.add(security) 136 | } 137 | 138 | @JvmOverloads 139 | fun withGlobalSecurity(name: String, security: Consumer = Consumer {}): SecurityComponentConfiguration = 140 | withGlobalSecurity(Security(name = name).also { security.accept(it) }) 141 | 142 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-openapi-plugin/src/main/kotlin/io/javalin/openapi/plugin/OpenApiHandler.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin 2 | 3 | import io.javalin.http.ContentType 4 | import io.javalin.http.Context 5 | import io.javalin.http.Handler 6 | import io.javalin.http.Header 7 | 8 | internal class OpenApiHandler(private val documentation: Lazy>) : Handler { 9 | 10 | override fun handle(context: Context) { 11 | context 12 | .header(Header.ACCESS_CONTROL_ALLOW_ORIGIN, "*") 13 | .header(Header.ACCESS_CONTROL_ALLOW_METHODS, "GET") 14 | .contentType(ContentType.JSON) 15 | .result(documentation.value[context.queryParamMap()["v"]?.firstOrNull() ?: "default"] ?: "{}") 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-openapi-plugin/src/main/kotlin/io/javalin/openapi/plugin/OpenApiPlugin.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude.Include 4 | import com.fasterxml.jackson.databind.JsonNode 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.fasterxml.jackson.databind.node.ObjectNode 7 | import io.javalin.config.JavalinConfig 8 | import io.javalin.openapi.OpenApiLoader 9 | import io.javalin.plugin.Plugin 10 | import java.util.function.Consumer 11 | 12 | open class OpenApiPlugin(userConfig: Consumer) : Plugin(userConfig, OpenApiPluginConfiguration()) { 13 | 14 | override fun onStart(config: JavalinConfig) { 15 | config.router.mount { 16 | it.get( 17 | pluginConfig.documentationPath, 18 | OpenApiHandler(createDocumentation()), 19 | *pluginConfig.roles?.toTypedArray() ?: emptyArray() 20 | ) 21 | } 22 | } 23 | 24 | private fun createDocumentation(): Lazy> = 25 | lazy { 26 | // skip nulls from cfg 27 | val jsonMapper = lazy { 28 | ObjectMapper().setSerializationInclusion(Include.NON_NULL) 29 | } 30 | 31 | OpenApiLoader() 32 | .loadOpenApiSchemes() 33 | .mapValues { (version, rawDocs) -> 34 | pluginConfig 35 | .definitionConfiguration 36 | ?.let { DefinitionConfiguration().also { definition -> it.accept(version, definition) } } 37 | ?.applyConfigurationTo( 38 | jsonMapper = jsonMapper.value, 39 | content = rawDocs, 40 | prettyOutputEnabled = pluginConfig.prettyOutputEnabled 41 | ) 42 | ?: rawDocs 43 | } 44 | } 45 | 46 | private fun DefinitionConfiguration.applyConfigurationTo(jsonMapper: ObjectMapper, content: String, prettyOutputEnabled: Boolean): String { 47 | val docsNode = jsonMapper.readTree(content) as ObjectNode 48 | 49 | //process OpenAPI "info" 50 | val updatedInfo = 51 | docsNode.get("info") 52 | ?.let { jsonMapper.readerForUpdating(it) } 53 | ?.readValue(jsonMapper.convertValue(info, JsonNode::class.java)) 54 | ?: jsonMapper.convertValue(info, JsonNode::class.java) 55 | docsNode.replace("info", updatedInfo) 56 | 57 | // process OpenAPI "servers" 58 | docsNode.replace("servers", jsonMapper.convertValue(servers, JsonNode::class.java)) 59 | 60 | // process OpenAPI "components" 61 | val componentsNode = docsNode.get("components") as? ObjectNode? 62 | ?: jsonMapper.createObjectNode().also { docsNode.replace("components", it) } 63 | 64 | // process OpenAPI "securitySchemes" 65 | val securitySchemes = security?.securitySchemes ?: emptyMap() 66 | componentsNode.replace("securitySchemes", jsonMapper.convertValue(securitySchemes, JsonNode::class.java)) 67 | 68 | //process OpenAPI "security" 69 | val securityMap = security?.globalSecurity?.map { mapOf(it.name to it.scopes.toTypedArray()) } 70 | docsNode.replace("security", jsonMapper.convertValue(securityMap, JsonNode::class.java)) 71 | 72 | return definitionProcessor 73 | ?.process(docsNode) 74 | ?: if (prettyOutputEnabled) docsNode.toPrettyString() else docsNode.toString() 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-openapi-plugin/src/test/kotlin/OpenApiPluginTest.kt: -------------------------------------------------------------------------------- 1 | import io.javalin.Javalin 2 | import io.javalin.openapi.OpenApi 3 | import io.javalin.openapi.plugin.OpenApiPlugin 4 | import kong.unirest.Unirest 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.Test 7 | 8 | class OpenApiPluginTest { 9 | 10 | @OpenApi( 11 | path = "/test", 12 | ) 13 | private object OpenApiTest 14 | 15 | @Test 16 | fun `should support schema modifications in definition configuration`() { 17 | val app = 18 | Javalin.createAndStart { config -> 19 | config.jetty.defaultPort = 0 20 | 21 | config.registerPlugin( 22 | OpenApiPlugin { openApiConfig -> 23 | openApiConfig.withDefinitionConfiguration { _, def -> 24 | def.withInfo { 25 | it.title = "My API" 26 | } 27 | } 28 | } 29 | ) 30 | } 31 | 32 | try { 33 | val response = Unirest.get("http://localhost:${app.port()}/openapi") 34 | .asString() 35 | .body 36 | 37 | assertThat(response).contains(""""title" : "My API"""") 38 | } finally { 39 | app.stop() 40 | } 41 | } 42 | 43 | @Test 44 | fun `should support empty definition configuration`() { 45 | val app = Javalin.createAndStart { config -> 46 | config.jetty.defaultPort = 0 47 | 48 | config.registerPlugin( 49 | OpenApiPlugin { 50 | it.withDefinitionConfiguration { _, _ -> 51 | /* do nothing */ 52 | } 53 | } 54 | ) 55 | } 56 | 57 | try { 58 | val response = Unirest.get("http://localhost:${app.port()}/openapi") 59 | .asString() 60 | .body 61 | 62 | assertThat(response).contains(""""title" : """"") 63 | } finally { 64 | app.stop() 65 | } 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-redoc-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Javalin ReDoc Plugin | Serve ReDoc UI for OpenAPI specification" 2 | 3 | dependencies { 4 | api(project(":openapi-specification")) 5 | 6 | implementation("org.webjars.npm:redoc:2.1.4") { // also bump redoc-ui version in OpenApiConfiguration 7 | exclude(group = "org.webjars.npm") 8 | } 9 | implementation("org.webjars.npm:js-tokens:8.0.2") 10 | } 11 | -------------------------------------------------------------------------------- /javalin-plugins/javalin-redoc-plugin/src/main/kotlin/io/javalin/openapi/plugin/redoc/ReDocHandler.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.redoc 2 | 3 | import io.javalin.http.Context 4 | import io.javalin.http.Handler 5 | 6 | /** 7 | * Based on https://github.com/tipsy/javalin/blob/master/javalin-openapi/src/main/java/io/javalin/plugin/openapi/ui/ReDocRenderer.kt by @chsfleury 8 | */ 9 | class ReDocHandler( 10 | private val title: String, 11 | private val documentationPath: String, 12 | private val version: String, 13 | private val routingPath: String, 14 | private val basePath: String? 15 | ) : Handler { 16 | 17 | override fun handle(context: Context) { 18 | context 19 | .html(createReDocUI()) 20 | .res().characterEncoding = "UTF-8" 21 | } 22 | 23 | private fun createReDocUI(): String { 24 | val rootPath = (basePath ?: "") + routingPath 25 | val publicBasePath = "$rootPath/webjars/redoc/$version".removedDoubledPathOperators() 26 | val publicDocumentationPath = (rootPath + documentationPath).removedDoubledPathOperators() 27 | 28 | return """ 29 | | 30 | | 31 | | 32 | | $title 33 | | 34 | | 35 | | 36 | | 37 | | 38 | | 39 | | 40 | | 41 | | 42 | | 43 | | 48 | | 49 | | 50 | |""".trimMargin() 51 | } 52 | 53 | private val multiplePathOperatorsRegex = Regex("/+") 54 | 55 | private fun String.removedDoubledPathOperators(): String = 56 | replace(multiplePathOperatorsRegex, "/") 57 | 58 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-redoc-plugin/src/main/kotlin/io/javalin/openapi/plugin/redoc/ReDocPlugin.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.redoc 2 | 3 | import io.javalin.config.JavalinConfig 4 | import io.javalin.plugin.Plugin 5 | import io.javalin.security.RouteRole 6 | import java.util.function.Consumer 7 | 8 | class ReDocConfiguration { 9 | /** Page title */ 10 | var title = "OpenApi documentation" 11 | /** ReDoc route */ 12 | var uiPath = "/redoc" 13 | /* Roles permitted to access ReDoc UI */ 14 | var roles: Array = emptyArray() 15 | /** Location of OpenApi documentation */ 16 | var documentationPath = "/openapi" 17 | /* Custom base path */ 18 | var basePath: String? = null 19 | /** ReDoc Bundle version **/ 20 | var version = "2.1.4" 21 | /** ReDoc WebJar route */ 22 | var webJarPath = "/webjars/redoc" 23 | } 24 | 25 | open class ReDocPlugin @JvmOverloads constructor(userConfig: Consumer = Consumer {}) : Plugin(userConfig, ReDocConfiguration()) { 26 | 27 | override fun onStart(config: JavalinConfig) { 28 | val reDocHandler = ReDocHandler( 29 | title = pluginConfig.title, 30 | documentationPath = pluginConfig.documentationPath, 31 | version = pluginConfig.version, 32 | routingPath = config.router.contextPath, 33 | basePath = pluginConfig.basePath 34 | ) 35 | 36 | val webJarHandler = ReDocWebJarHandler( 37 | redocWebJarPath = pluginConfig.webJarPath 38 | ) 39 | 40 | config.router.mount { router -> 41 | router 42 | .get(pluginConfig.uiPath, reDocHandler, *pluginConfig.roles) 43 | .get("${pluginConfig.webJarPath}/*", webJarHandler, *pluginConfig.roles) 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-redoc-plugin/src/main/kotlin/io/javalin/openapi/plugin/redoc/ReDocWebJarHandler.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.redoc 2 | 3 | import io.javalin.http.Context 4 | import io.javalin.http.Handler 5 | import org.eclipse.jetty.http.HttpStatus 6 | import org.eclipse.jetty.http.MimeTypes 7 | 8 | internal class ReDocWebJarHandler(private val redocWebJarPath: String) : Handler { 9 | 10 | override fun handle(context: Context) { 11 | val resource = ReDocPlugin::class.java.getResourceAsStream("/META-INF/resources" + redocWebJarPath + context.path().replaceFirst(context.contextPath(), "").replaceFirst(redocWebJarPath, "")) 12 | 13 | if (resource == null) { 14 | context.status(HttpStatus.NOT_FOUND_404) 15 | return 16 | } 17 | 18 | context.res().characterEncoding = "UTF-8" 19 | context.result(resource) 20 | 21 | MimeTypes.getDefaultMimeByExtension(context.path())?.let { 22 | context.contentType(it) 23 | } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-redoc-plugin/src/test/kotlin/io/javalin/openapi/plugin/redoc/RedocPluginTest.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.redoc 2 | 3 | import io.javalin.Javalin 4 | import io.javalin.openapi.plugin.redoc.specification.JavalinBehindProxy 5 | import kong.unirest.Unirest 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | 9 | internal class RedocPluginTest { 10 | 11 | @Test 12 | fun `should properly host redoc ui`() { 13 | val app = Javalin.createAndStart { it.registerPlugin(ReDocPlugin()) } 14 | 15 | try { 16 | val response = Unirest.get("http://localhost:8080/redoc") 17 | .asString() 18 | .body 19 | 20 | assertThat(response).contains("""src="/webjars/redoc/${ReDocConfiguration().version}/bundles/redoc.standalone.js"""") 21 | assertThat(response).contains("""'/openapi'""") 22 | } finally { 23 | app.stop() 24 | } 25 | } 26 | 27 | @Test 28 | fun `should support custom base path`() { 29 | JavalinBehindProxy( 30 | javalinSupplier = { Javalin.create { it.registerPlugin(ReDocPlugin { redoc -> redoc.basePath = "/custom" }) } }, 31 | port = 8080, 32 | basePath = "/custom" 33 | ).use { 34 | val response = Unirest.get("http://localhost:8080/custom/redoc") 35 | .asString() 36 | .body 37 | 38 | assertThat(response).contains("""src="/custom/webjars/redoc/${ReDocConfiguration().version}/bundles/redoc.standalone.js"""") 39 | assertThat(response).contains("""'/custom/openapi'""") 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-redoc-plugin/src/test/kotlin/io/javalin/openapi/plugin/redoc/specification/JavalinBehindProxy.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.redoc.specification 2 | 3 | import io.javalin.Javalin 4 | import io.javalin.http.Context 5 | import kong.unirest.HttpRequest 6 | import kong.unirest.Unirest 7 | import java.util.concurrent.CountDownLatch 8 | import java.util.function.Supplier 9 | 10 | internal class JavalinBehindProxy( 11 | javalinSupplier: Supplier, 12 | private val port: Int, 13 | basePath: String 14 | ) : AutoCloseable { 15 | 16 | private val javalin = javalinSupplier 17 | .get() 18 | 19 | private val proxy = Javalin.create() 20 | .get("/") { it.html("Index") } 21 | .get(basePath) { Unirest.get(it.javalinLocation()).redirect(it) } 22 | .get("$basePath/") { Unirest.get(it.javalinLocation()).redirect(it) } 23 | .head("$basePath/") { Unirest.head(it.javalinLocation()).redirect(it) } 24 | .post("$basePath/") { Unirest.post(it.javalinLocation()).redirect(it) } 25 | .put("$basePath/") { Unirest.put(it.javalinLocation()).redirect(it) } 26 | .delete("$basePath/") { Unirest.delete(it.javalinLocation()).redirect(it) } 27 | .options("$basePath/") { Unirest.options(it.javalinLocation()).redirect(it) } 28 | 29 | init { 30 | start() 31 | } 32 | 33 | fun start(): JavalinBehindProxy = also { 34 | val awaitStart = CountDownLatch(2) 35 | 36 | javalin 37 | .events { it.serverStarted { awaitStart.countDown() } } 38 | .start(port + 1) 39 | 40 | proxy 41 | .events { it.serverStarted { awaitStart.countDown() } } 42 | .start(port) 43 | 44 | awaitStart.await() 45 | } 46 | 47 | fun stop() { 48 | proxy.stop() 49 | javalin.stop() 50 | } 51 | 52 | override fun close() { 53 | stop() 54 | } 55 | 56 | private fun > R.redirect(ctx: Context) { 57 | ctx.headerMap().forEach { (key, value) -> header(key, value) } 58 | val response = this.asBytes() 59 | response.headers.all().forEach { ctx.header(it.name, it.value) } 60 | ctx.status(response.status).result(response.body) 61 | } 62 | 63 | private fun Context.javalinLocation(): String = 64 | "http://localhost:${port + 1}/${pathParamMap()["uri"] ?: ""}" 65 | 66 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-redoc-plugin/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /javalin-plugins/javalin-redoc-plugin/src/test/resources/openapi-plugin/.index: -------------------------------------------------------------------------------- 1 | test.json -------------------------------------------------------------------------------- /javalin-plugins/javalin-redoc-plugin/src/test/resources/openapi-plugin/test.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /javalin-plugins/javalin-swagger-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Javalin Swagger Plugin | Serve Swagger UI for OpenAPI specification" 2 | 3 | dependencies { 4 | api(project(":openapi-specification")) 5 | @Suppress("GradlePackageUpdate") 6 | api("org.webjars:swagger-ui:5.17.14") // also bump swagger-ui version in OpenApiConfiguration 7 | } 8 | -------------------------------------------------------------------------------- /javalin-plugins/javalin-swagger-plugin/src/main/kotlin/io/javalin/openapi/plugin/swagger/SwaggerHandler.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.swagger 2 | 3 | import io.javalin.http.Context 4 | import io.javalin.http.Handler 5 | import io.javalin.http.HandlerType 6 | import io.javalin.router.Endpoint 7 | import io.javalin.security.RouteRole 8 | import org.intellij.lang.annotations.Language 9 | 10 | class SwaggerEndpoint( 11 | method: HandlerType, 12 | path: String, 13 | roles: Set, 14 | handler: Handler 15 | ) : Endpoint( 16 | method = method, 17 | path = path, 18 | roles = roles.toTypedArray(), 19 | handler = handler 20 | ) 21 | 22 | /** 23 | * Based on https://github.com/tipsy/javalin/blob/master/javalin-openapi/src/main/java/io/javalin/plugin/openapi/ui/SwaggerRenderer.kt by @chsfleury 24 | */ 25 | class SwaggerHandler( 26 | private val title: String, 27 | private val documentationPath: String, 28 | private val versions: Set, 29 | private val swaggerVersion: String, 30 | private val validatorUrl: String?, 31 | private val routingPath: String, 32 | private val basePath: String?, 33 | private val tagsSorter: String, 34 | private val operationsSorter: String, 35 | private val customStylesheetFiles: List>, 36 | private val customJavaScriptFiles: List> 37 | ) : Handler { 38 | 39 | private val swaggerUiHtml = createSwaggerUiHtml() 40 | 41 | override fun handle(context: Context) { 42 | context 43 | .html(swaggerUiHtml) 44 | .res() 45 | .characterEncoding = "UTF-8" 46 | } 47 | 48 | private fun createSwaggerUiHtml(): String { 49 | val rootPath = (basePath ?: "") + routingPath 50 | val publicSwaggerAssetsPath = "$rootPath/webjars/swagger-ui/$swaggerVersion".removedDoubledPathOperators() 51 | val publicDocumentationPath = (rootPath + documentationPath).removedDoubledPathOperators() 52 | val allDocumentations = versions 53 | .joinToString(separator = ",\n") { "{ name: '$it', url: '$publicDocumentationPath?v=$it' }" } 54 | val allCustomStylesheets = customStylesheetFiles 55 | .joinToString(separator = "\n") { "" } 56 | val allCustomJavaScripts = customJavaScriptFiles 57 | .joinToString(separator = "\n") { " 88 | 89 | 111 | $allCustomJavaScripts 112 | 113 | 114 | """.trimIndent() 115 | 116 | return html 117 | } 118 | 119 | private fun String.removedDoubledPathOperators(): String { 120 | val multiplePathOperatorsRegex = Regex("/+") 121 | return replace(multiplePathOperatorsRegex, "/") 122 | } 123 | 124 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-swagger-plugin/src/main/kotlin/io/javalin/openapi/plugin/swagger/SwaggerPlugin.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.swagger 2 | 3 | import io.javalin.config.JavalinConfig 4 | import io.javalin.http.HandlerType 5 | import io.javalin.http.HandlerType.GET 6 | import io.javalin.openapi.OpenApiLoader 7 | import io.javalin.plugin.Plugin 8 | import io.javalin.security.RouteRole 9 | import java.util.function.Consumer 10 | 11 | class SwaggerConfiguration { 12 | /** Location of OpenApi documentation */ 13 | var documentationPath = "/openapi" 14 | /* Swagger UI route */ 15 | var uiPath = "/swagger" 16 | /** Roles eligible to connect to Swagger routes */ 17 | var roles: Array = emptyArray() 18 | /** Specify custom base path if Javalin is running behind reverse proxy */ 19 | var basePath: String? = null 20 | 21 | // WebJar configuration 22 | /** Swagger UI Bundle version */ 23 | var version = "5.17.14" 24 | /** Swagger UI Bundler webjar location */ 25 | var webJarPath = "/webjars/swagger-ui" 26 | 27 | // Swagger UI bundle configuration 28 | // ~ https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ */ 29 | /** Page title **/ 30 | var title = "OpenApi documentation" 31 | /** Specification validator */ 32 | var validatorUrl: String? = "https://validator.swagger.io/validator" 33 | /** Tags sorter algorithm expression. */ 34 | var tagsSorter: String = "'alpha'" 35 | /** Operations sorter algorithm expression. */ 36 | var operationsSorter: String = "'alpha'" 37 | /** Custom CSS files to be injected into Swagger HTML */ 38 | var customStylesheetFiles: MutableList> = arrayListOf() 39 | /** Custom JavaScript files to be injected into Swagger HTML */ 40 | var customJavaScriptFiles: MutableList> = arrayListOf() 41 | 42 | @JvmOverloads 43 | fun injectStylesheet(path: String, media: String = "screen"): SwaggerConfiguration = also { 44 | customStylesheetFiles.add(Pair(path, media)); 45 | } 46 | 47 | @JvmOverloads 48 | fun injectJavaScript(path: String, type: String = "text/javascript"): SwaggerConfiguration = also { 49 | customJavaScriptFiles.add(Pair(path, type)) 50 | } 51 | } 52 | 53 | open class SwaggerPlugin @JvmOverloads constructor(userConfig: Consumer = Consumer {}) : Plugin(userConfig, SwaggerConfiguration()) { 54 | 55 | override fun onStart(config: JavalinConfig) { 56 | val versions = OpenApiLoader() 57 | .loadVersions() 58 | 59 | val swaggerHandler = SwaggerHandler( 60 | title = pluginConfig.title, 61 | documentationPath = pluginConfig.documentationPath, 62 | versions = versions, 63 | swaggerVersion = pluginConfig.version, 64 | validatorUrl = pluginConfig.validatorUrl, 65 | routingPath = config.router.contextPath, 66 | basePath = pluginConfig.basePath, 67 | tagsSorter = pluginConfig.tagsSorter, 68 | operationsSorter = pluginConfig.operationsSorter, 69 | customStylesheetFiles = pluginConfig.customStylesheetFiles, 70 | customJavaScriptFiles = pluginConfig.customJavaScriptFiles 71 | ) 72 | 73 | val swaggerEndpoint = SwaggerEndpoint( 74 | method = HandlerType.GET, 75 | path = pluginConfig.uiPath, 76 | roles = pluginConfig.roles.toSet(), 77 | handler = swaggerHandler 78 | ) 79 | 80 | config.router.mount { router -> 81 | /** Register handler for swagger ui */ 82 | router.addEndpoint(swaggerEndpoint) 83 | 84 | /** Register webjar handler if and only if there isn't already a [SwaggerWebJarHandler] at configured route */ 85 | config.pvt.internalRouter 86 | .findHttpHandlerEntries(HandlerType.GET, "${pluginConfig.webJarPath}/*") 87 | .takeIf { routes -> routes.noneMatch { it.endpoint is SwaggerEndpoint } } 88 | ?.run { 89 | val swaggerWebJarHandler = SwaggerWebJarHandler( 90 | swaggerWebJarPath = pluginConfig.webJarPath 91 | ) 92 | router.addEndpoint( 93 | SwaggerEndpoint( 94 | method = GET, 95 | path = "${pluginConfig.webJarPath}/*", 96 | roles = pluginConfig.roles.toSet(), 97 | handler = swaggerWebJarHandler 98 | ) 99 | ) 100 | } 101 | } 102 | } 103 | 104 | override fun repeatable(): Boolean = 105 | true 106 | 107 | } 108 | -------------------------------------------------------------------------------- /javalin-plugins/javalin-swagger-plugin/src/main/kotlin/io/javalin/openapi/plugin/swagger/SwaggerWebJarHandler.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.swagger 2 | 3 | import io.javalin.http.Context 4 | import io.javalin.http.Handler 5 | import org.eclipse.jetty.http.HttpStatus 6 | import org.eclipse.jetty.http.MimeTypes 7 | import java.io.InputStream 8 | 9 | internal class SwaggerWebJarHandler( 10 | private val swaggerWebJarPath: String, 11 | ) : Handler { 12 | 13 | override fun handle(context: Context) { 14 | val resourceRootPath = "/META-INF/resources$swaggerWebJarPath" 15 | 16 | val requestedResource = context.path() 17 | .replaceFirst(context.contextPath(), "") 18 | .replaceFirst(swaggerWebJarPath, "") 19 | 20 | val resource: InputStream? = SwaggerPlugin::class.java.getResourceAsStream(resourceRootPath + requestedResource) 21 | 22 | if (resource == null) { 23 | context.status(HttpStatus.NOT_FOUND_404) 24 | return 25 | } 26 | 27 | context.result(resource) 28 | context.res().characterEncoding = "UTF-8" 29 | 30 | MimeTypes.getDefaultMimeByExtension(context.path())?.let { // Swagger returns various non-standard assets like .js.map that are not recognized 31 | context.contentType(it) 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-swagger-plugin/src/test/kotlin/io/javalin/openapi/plugin/swagger/SwaggerPluginTest.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.swagger 2 | 3 | import io.javalin.Javalin 4 | import io.javalin.openapi.plugin.swagger.specification.JavalinBehindProxy 5 | import kong.unirest.Unirest 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | 9 | internal class SwaggerPluginTest { 10 | 11 | @Test 12 | fun `should properly host swagger ui`() { 13 | val app = Javalin.createAndStart { it.registerPlugin(SwaggerPlugin()) } 14 | 15 | try { 16 | val response = Unirest.get("http://localhost:8080/swagger") 17 | .asString() 18 | .body 19 | 20 | assertThat(response).contains("""href="/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui.css"""") 21 | assertThat(response).contains("""src="/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui-bundle.js"""") 22 | assertThat(response).contains("""src="/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui-standalone-preset.js"""") 23 | assertThat(response).contains("""url: '/openapi?v=test'""") 24 | } finally { 25 | app.stop() 26 | } 27 | } 28 | 29 | @Test 30 | fun `should support custom base path`() { 31 | JavalinBehindProxy( 32 | javalinSupplier = { Javalin.create { it.registerPlugin(SwaggerPlugin { it.basePath = "/custom" }) } }, 33 | port = 8080, 34 | basePath = "/custom" 35 | ).use { 36 | val response = Unirest.get("http://localhost:8080/custom/swagger") 37 | .asString() 38 | .body 39 | 40 | assertThat(response).contains("""href="/custom/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui.css"""") 41 | assertThat(response).contains("""src="/custom/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui-bundle.js"""") 42 | assertThat(response).contains("""src="/custom/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui-standalone-preset.js"""") 43 | assertThat(response).contains("""url: '/custom/openapi?v=test'""") 44 | } 45 | } 46 | 47 | @Test 48 | fun `should have custom css and js injected`() { 49 | val app = Javalin.createAndStart { it.registerPlugin(SwaggerPlugin { swagger -> 50 | swagger 51 | .injectStylesheet("/swagger.css") 52 | .injectStylesheet("/swagger-the-print.css", "print") 53 | .injectJavaScript("/script.js") 54 | }) } 55 | 56 | try { 57 | val response = Unirest.get("http://localhost:8080/swagger") 58 | .asString() 59 | .body 60 | 61 | assertThat(response).contains("""link href='/swagger.css' rel='stylesheet' media='screen' type='text/css'""") 62 | assertThat(response).contains("""link href='/swagger-the-print.css' rel='stylesheet' media='print' type='text/css'""") 63 | assertThat(response).contains("""script src='/script.js' type='text/javascript'""") 64 | } finally { 65 | app.stop() 66 | } 67 | } 68 | 69 | @Test 70 | fun `should not fail if second swagger plugin is registered`() { 71 | val app = Javalin.createAndStart { 72 | it.registerPlugin(SwaggerPlugin()) 73 | it.registerPlugin(SwaggerPlugin { swagger -> 74 | swagger.documentationPath = "/example-docs" 75 | swagger.uiPath = "/example-ui" 76 | }) 77 | } 78 | 79 | try { 80 | val javalinHost = "http://localhost:8080" 81 | val webjarJsRoute = "/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui-bundle.js" 82 | 83 | val response = Unirest.get("$javalinHost/swagger") 84 | .asString() 85 | .body 86 | 87 | assertThat(response).contains("""src="$webjarJsRoute"""") 88 | assertThat(response).contains("""url: '/openapi?v=test'""") 89 | assertThat(response).doesNotContain("""url: '/example-docs?v=test'""") 90 | 91 | val resourceResponse = Unirest.get("$javalinHost$webjarJsRoute") 92 | .asString() 93 | .body 94 | 95 | assertThat(resourceResponse).isNotBlank 96 | 97 | val otherResponse = Unirest.get("$javalinHost/example-ui") 98 | .asString() 99 | .body 100 | 101 | assertThat(otherResponse).contains("""url: '/example-docs?v=test'""") 102 | assertThat(otherResponse).doesNotContain("""url: '/openapi?v=test'""") 103 | } finally { 104 | app.stop() 105 | } 106 | } 107 | 108 | @Test 109 | fun `should not fail if second swagger plugin is registered with routes`(){ 110 | val app = Javalin.createAndStart { 111 | it.registerPlugin(SwaggerPlugin()) 112 | it.registerPlugin(SwaggerPlugin { swagger -> 113 | swagger.documentationPath = "/example-docs" 114 | swagger.uiPath = "/example-ui" 115 | }) 116 | it.router.mount { cfg -> 117 | cfg.get("/some/route/") { ctx -> ctx.result("Hello World") } 118 | } 119 | } 120 | 121 | try { 122 | val javalinHost = "http://localhost:8080" 123 | 124 | val webjarCssRoute = "/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui.css" 125 | val webjarJsRoute = "/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui-bundle.js" 126 | val webjarJsStandaloneRoute = "/webjars/swagger-ui/${SwaggerConfiguration().version}/swagger-ui-standalone-preset.js" 127 | 128 | val response = Unirest.get("$javalinHost/swagger") 129 | .asString() 130 | .body 131 | 132 | assertThat(response).contains("""href="$webjarCssRoute"""") 133 | assertThat(response).contains("""src="$webjarJsRoute"""") 134 | assertThat(response).contains("""src="$webjarJsStandaloneRoute"""") 135 | assertThat(response).contains("""url: '/openapi?v=test'""") 136 | assertThat(response).doesNotContain("""url: '/example-docs?v=test'""") 137 | 138 | var resourceResponse = Unirest.get("$javalinHost$webjarCssRoute") 139 | .asString() 140 | .body 141 | 142 | assertThat(resourceResponse).isNotBlank 143 | 144 | resourceResponse = Unirest.get("$javalinHost$webjarJsRoute") 145 | .asString() 146 | .body 147 | 148 | assertThat(resourceResponse).isNotBlank 149 | 150 | resourceResponse = Unirest.get("$javalinHost$webjarJsStandaloneRoute") 151 | .asString() 152 | .body 153 | 154 | assertThat(resourceResponse).isNotBlank 155 | 156 | val otherResponse = Unirest.get("$javalinHost/example-ui") 157 | .asString() 158 | .body 159 | 160 | assertThat(otherResponse).contains("""url: '/example-docs?v=test'""") 161 | assertThat(otherResponse).doesNotContain("""url: '/openapi?v=test'""") 162 | } finally { 163 | app.stop() 164 | } 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /javalin-plugins/javalin-swagger-plugin/src/test/kotlin/io/javalin/openapi/plugin/swagger/specification/JavalinBehindProxy.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.plugin.swagger.specification 2 | 3 | import io.javalin.Javalin 4 | import io.javalin.http.Context 5 | import kong.unirest.HttpRequest 6 | import kong.unirest.Unirest 7 | import java.util.concurrent.CountDownLatch 8 | import java.util.function.Supplier 9 | 10 | internal class JavalinBehindProxy( 11 | javalinSupplier: Supplier, 12 | private val port: Int, 13 | basePath: String 14 | ) : AutoCloseable { 15 | 16 | private val javalin = javalinSupplier 17 | .get() 18 | 19 | private val proxy = Javalin.create() 20 | .get("/") { it.html("Index") } 21 | .get(basePath) { Unirest.get(it.javalinLocation()).redirect(it) } 22 | .get("$basePath/") { Unirest.get(it.javalinLocation()).redirect(it) } 23 | .head("$basePath/") { Unirest.head(it.javalinLocation()).redirect(it) } 24 | .post("$basePath/") { Unirest.post(it.javalinLocation()).redirect(it) } 25 | .put("$basePath/") { Unirest.put(it.javalinLocation()).redirect(it) } 26 | .delete("$basePath/") { Unirest.delete(it.javalinLocation()).redirect(it) } 27 | .options("$basePath/") { Unirest.options(it.javalinLocation()).redirect(it) } 28 | 29 | init { 30 | start() 31 | } 32 | 33 | fun start(): JavalinBehindProxy = also { 34 | val awaitStart = CountDownLatch(2) 35 | 36 | javalin 37 | .events { it.serverStarted { awaitStart.countDown() } } 38 | .start(port + 1) 39 | 40 | proxy 41 | .events { it.serverStarted { awaitStart.countDown() } } 42 | .start(port) 43 | 44 | awaitStart.await() 45 | } 46 | 47 | fun stop() { 48 | proxy.stop() 49 | javalin.stop() 50 | } 51 | 52 | override fun close() { 53 | stop() 54 | } 55 | 56 | private fun > R.redirect(ctx: Context) { 57 | ctx.headerMap().forEach { (key, value) -> header(key, value) } 58 | val response = this.asBytes() 59 | response.headers.all().forEach { ctx.header(it.name, it.value) } 60 | ctx.status(response.status).result(response.body) 61 | } 62 | 63 | private fun Context.javalinLocation(): String = 64 | "http://localhost:${port + 1}/${pathParamMap()["uri"] ?: ""}" 65 | 66 | } -------------------------------------------------------------------------------- /javalin-plugins/javalin-swagger-plugin/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /javalin-plugins/javalin-swagger-plugin/src/test/resources/openapi-plugin/.index: -------------------------------------------------------------------------------- 1 | test.json -------------------------------------------------------------------------------- /javalin-plugins/javalin-swagger-plugin/src/test/resources/openapi-plugin/test.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /openapi-annotation-processor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs 2 | 3 | description = "Javalin OpenAPI Annotation Processor | Generates OpenApi specification from @OpenApi annotations" 4 | 5 | plugins { 6 | kotlin("kapt") 7 | } 8 | 9 | dependencies { 10 | api(project(":openapi-specification")) 11 | kaptTest(project(":openapi-annotation-processor")) 12 | testImplementation(project(":openapi-annotation-processor")) 13 | 14 | implementation(kotlin("reflect")) 15 | implementation("org.apache.groovy:groovy:4.0.21") 16 | 17 | implementation("io.javalin:javalin:6.6.0") { 18 | exclude(group = "org.slf4j") 19 | } 20 | 21 | implementation("io.swagger.parser.v3:swagger-parser:2.1.15") 22 | 23 | implementation("ch.qos.logback:logback-classic:1.4.14") 24 | 25 | testImplementation("org.mongodb:bson:4.9.0") 26 | } 27 | 28 | tasks.withType { 29 | dependsOn( 30 | ":openapi-annotation-processor:clean", 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /openapi-annotation-processor/src/main/kotlin/io/javalin/openapi/processor/AnnotationProcessorTools.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.processor 2 | 3 | import com.sun.source.util.Trees 4 | import io.javalin.openapi.experimental.processor.shared.printException 5 | import java.lang.reflect.Proxy 6 | import javax.annotation.processing.ProcessingEnvironment 7 | import javax.tools.Diagnostic.Kind.NOTE 8 | 9 | 10 | object AnnotationProcessorTools { 11 | 12 | /** 13 | * GH-141 Support IntelliJ's ProcessingEnvironment 14 | * ~ https://github.com/javalin/javalin-openapi/issues/141 15 | */ 16 | fun createTrees(processingEnvironment: ProcessingEnvironment): Trees? = 17 | runCatching { 18 | unwrap(processingEnvironment) 19 | ?.let { Trees.instance(it) } 20 | ?: Trees.instance(processingEnvironment) 21 | }.getOrNull() 22 | 23 | private fun unwrap(processingEnv: ProcessingEnvironment): ProcessingEnvironment? = 24 | when { 25 | Proxy.isProxyClass(processingEnv.javaClass) -> { 26 | val invocationHandler = Proxy.getInvocationHandler(processingEnv) 27 | 28 | try { 29 | val field = invocationHandler.javaClass.getDeclaredField("val\$delegateTo") 30 | field.isAccessible = true 31 | 32 | when (val delegateTo = field.get(invocationHandler)) { 33 | is ProcessingEnvironment -> delegateTo 34 | else -> { 35 | processingEnv.messager.printMessage(NOTE, "got ${delegateTo.javaClass} expected instanceof com.sun.tools.javac.processing.JavacProcessingEnvironment") 36 | null 37 | } 38 | } 39 | } catch (exception: Exception) { 40 | processingEnv.messager.printException(NOTE, exception) 41 | null 42 | } 43 | } 44 | else -> processingEnv 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/main/kotlin/io/javalin/openapi/processor/OpenApiAnnotationProcessor.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.processor 2 | 3 | import io.javalin.openapi.experimental.ExperimentalCompileOpenApiConfiguration 4 | import io.javalin.openapi.JsonSchema 5 | import io.javalin.openapi.OpenApi 6 | import io.javalin.openapi.experimental.AnnotationProcessorContext 7 | import io.javalin.openapi.experimental.OPENAPI_INFO_TITLE 8 | import io.javalin.openapi.experimental.OPENAPI_INFO_VERSION 9 | import io.javalin.openapi.experimental.OpenApiAnnotationProcessorConfiguration 10 | import io.javalin.openapi.experimental.OpenApiAnnotationProcessorParameters 11 | import io.javalin.openapi.experimental.OpenApiAnnotationProcessorParameters.Info 12 | import io.javalin.openapi.processor.configuration.OpenApiPrecompileScriptingEngine 13 | import io.javalin.openapi.processor.generators.JsonSchemaGenerator 14 | import io.javalin.openapi.processor.generators.OpenApiGenerator 15 | import javax.annotation.processing.AbstractProcessor 16 | import javax.annotation.processing.ProcessingEnvironment 17 | import javax.annotation.processing.RoundEnvironment 18 | import javax.lang.model.SourceVersion 19 | import javax.lang.model.element.TypeElement 20 | import javax.tools.Diagnostic.Kind.NOTE 21 | 22 | open class OpenApiAnnotationProcessor : AbstractProcessor() { 23 | 24 | companion object { 25 | internal lateinit var context: AnnotationProcessorContext 26 | } 27 | 28 | override fun init(processingEnv: ProcessingEnvironment) { 29 | context = AnnotationProcessorContext( 30 | parameters = OpenApiAnnotationProcessorParameters( 31 | info = Info( 32 | title = processingEnv.options[OPENAPI_INFO_TITLE] ?: "", 33 | version = processingEnv.options[OPENAPI_INFO_VERSION] ?: "" 34 | ) 35 | ), 36 | configuration = OpenApiAnnotationProcessorConfiguration(), 37 | env = processingEnv, 38 | trees = AnnotationProcessorTools.createTrees(processingEnv) 39 | ) 40 | } 41 | 42 | @OptIn(ExperimentalCompileOpenApiConfiguration::class) 43 | override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { 44 | if (roundEnv.processingOver()) { 45 | return false 46 | } 47 | context.roundEnv = roundEnv 48 | 49 | val openApiPrecompileScriptingEngine = OpenApiPrecompileScriptingEngine() 50 | val configurer = openApiPrecompileScriptingEngine.load(roundEnv) 51 | configurer?.configure(context.configuration) 52 | 53 | context.inDebug { 54 | it.printMessage(NOTE, "OpenApi | Debug mode enabled") 55 | } 56 | 57 | val openApiGenerator = OpenApiGenerator() 58 | openApiGenerator.generate(roundEnv) 59 | 60 | val jsonSchemaGenerator = JsonSchemaGenerator() 61 | jsonSchemaGenerator.generate(roundEnv) 62 | 63 | return true 64 | } 65 | 66 | override fun getSupportedOptions(): Set = 67 | setOf( 68 | OPENAPI_INFO_TITLE, 69 | OPENAPI_INFO_VERSION, 70 | ) 71 | 72 | override fun getSupportedAnnotationTypes(): Set = 73 | setOf( 74 | OpenApi::class.qualifiedName!!, 75 | JsonSchema::class.qualifiedName!!, 76 | ) 77 | 78 | override fun getSupportedSourceVersion(): SourceVersion = 79 | SourceVersion.latestSupported() 80 | 81 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/main/kotlin/io/javalin/openapi/processor/configuration/OpenApiPrecompileScriptingEngine.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.processor.configuration 2 | 3 | import groovy.lang.GroovyClassLoader 4 | import io.javalin.openapi.JsonSchema 5 | import io.javalin.openapi.OpenApi 6 | import io.javalin.openapi.OpenApis 7 | import io.javalin.openapi.experimental.ExperimentalCompileOpenApiConfiguration 8 | import io.javalin.openapi.experimental.OpenApiAnnotationProcessorConfigurer 9 | import io.javalin.openapi.experimental.processor.shared.info 10 | import io.javalin.openapi.processor.OpenApiAnnotationProcessor.Companion.context 11 | import java.io.File 12 | import javax.annotation.processing.RoundEnvironment 13 | 14 | class OpenApiPrecompileScriptingEngine { 15 | 16 | private val classLoader = OpenApiPrecompileScriptingEngine::class.java.classLoader 17 | private val groovyClassLoader by lazy { GroovyClassLoader(classLoader) } 18 | 19 | @OptIn(ExperimentalCompileOpenApiConfiguration::class) 20 | fun load(roundEnvironment: RoundEnvironment): OpenApiAnnotationProcessorConfigurer? = 21 | roundEnvironment.getElementsAnnotatedWithAny(setOf(OpenApis::class.java, OpenApi::class.java, JsonSchema::class.java)) 22 | .firstOrNull() 23 | ?.let { context.trees?.getPath(it)?.compilationUnit?.sourceFile?.name } 24 | ?.let { 25 | when { 26 | /* Default sources */ 27 | it.contains("src".toPathSegmentIdentifier()) -> it.findSourceTargetNameBy("src") to it.substringBeforeLast("src".toPathSegmentIdentifier()) 28 | /* Kapt stubs */ 29 | it.contains("stubs".toPathSegmentIdentifier()) -> it.findSourceTargetNameBy("stubs") to it.substringBeforeLast("build".toPathSegmentIdentifier()) 30 | else -> null 31 | } 32 | } 33 | ?.let { (sourceTargetName, compileSources) -> File(compileSources).resolve("src").resolve(sourceTargetName).resolve("compile").resolve("openapi.groovy") } 34 | ?.also { context.env.messager.info(it.absolutePath.toString()) } 35 | ?.takeIf { it.exists() } 36 | ?.let { scriptFile -> groovyClassLoader.parseClass(scriptFile).getConstructor().newInstance() as OpenApiAnnotationProcessorConfigurer } 37 | 38 | private fun String.findSourceTargetNameBy(segment: String): String = 39 | substringAfter(segment.toPathSegmentIdentifier()).substringBefore(File.separator) 40 | 41 | private fun String.toPathSegmentIdentifier(): String = 42 | File.separator + this + File.separator 43 | 44 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/main/kotlin/io/javalin/openapi/processor/generators/JsonSchemaGenerator.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.processor.generators 2 | 3 | import com.google.gson.JsonObject 4 | import io.javalin.openapi.JsonSchema 5 | import io.javalin.openapi.experimental.processor.shared.saveResource 6 | import io.javalin.openapi.experimental.processor.shared.toPrettyString 7 | import io.javalin.openapi.processor.OpenApiAnnotationProcessor.Companion.context 8 | import javax.annotation.processing.RoundEnvironment 9 | import javax.lang.model.element.Element 10 | 11 | class JsonSchemaGenerator { 12 | 13 | fun generate(roundEnvironment: RoundEnvironment) = 14 | roundEnvironment.getElementsAnnotatedWith(JsonSchema::class.java) 15 | .filter { it.getAnnotation(JsonSchema::class.java).generateResource } 16 | .onEach { context.env.filer.saveResource(context, "json-schemes/${it}", generate(it)) } 17 | .run { context.env.filer.saveResource(context, "json-schemes/index", joinToString(separator = "\n")) } 18 | 19 | private fun generate(element: Element): String { 20 | val scheme = JsonObject() 21 | scheme.addProperty("\$schema", "http://json-schema.org/draft-07/schema#") 22 | 23 | context.inContext { 24 | val (entityScheme) = context.typeSchemaGenerator.createTypeSchema( 25 | type = element.asType().toClassDefinition(), 26 | inlineRefs = true 27 | ) 28 | 29 | entityScheme.entrySet().forEach { (key, value) -> 30 | scheme.add(key, value) 31 | } 32 | } 33 | 34 | return scheme.toPrettyString() 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | io.javalin.openapi.processor.OpenApiAnnotationProcessor -------------------------------------------------------------------------------- /openapi-annotation-processor/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/compile/openapi.groovy: -------------------------------------------------------------------------------- 1 | import io.javalin.openapi.experimental.AnnotationProcessorContext 2 | import io.javalin.openapi.experimental.ClassDefinition 3 | import io.javalin.openapi.experimental.EmbeddedTypeProcessorContext 4 | import io.javalin.openapi.experimental.ExperimentalCompileOpenApiConfiguration 5 | import io.javalin.openapi.experimental.OpenApiAnnotationProcessorConfiguration 6 | import io.javalin.openapi.experimental.OpenApiAnnotationProcessorConfigurer 7 | import io.javalin.openapi.experimental.SimpleType 8 | 9 | import javax.lang.model.element.Element 10 | import javax.lang.model.element.TypeElement 11 | 12 | @ExperimentalCompileOpenApiConfiguration 13 | class OpenApiConfiguration implements OpenApiAnnotationProcessorConfigurer { 14 | 15 | @Override 16 | void configure(OpenApiAnnotationProcessorConfiguration configuration) { 17 | configuration.validateWithParser = false 18 | // configuration.debug = false 19 | 20 | // Used by TypeMappersTest 21 | configuration.simpleTypeMappings['io.javalin.openapi.processor.TypeMappersTest.CustomType'] = new SimpleType("string") 22 | 23 | // Used by UserCasesTest 24 | configuration.propertyInSchemeFilter = { AnnotationProcessorContext ctx, ClassDefinition type, Element property -> 25 | TypeElement specificRecord = ctx.forTypeElement('io.javalin.openapi.processor.UserCasesTest.SpecificRecord') 26 | TypeElement specificRecordBase = ctx.forTypeElement('io.javalin.openapi.processor.UserCasesTest.SpecificRecordBase') 27 | 28 | if (ctx.isAssignable(type.mirror, specificRecord.asType()) && ctx.hasElement(specificRecord, property)) { 29 | return false // exclude 30 | } 31 | 32 | if (ctx.isAssignable(type.mirror, specificRecordBase.asType()) && ctx.hasElement(specificRecordBase, property)) { 33 | return false // exclude 34 | } 35 | 36 | return true // include 37 | } 38 | 39 | // Used by CustomTypeMappingsTest 40 | configuration.insertEmbeddedTypeProcessor({ EmbeddedTypeProcessorContext context -> 41 | if (context.type.simpleName == 'Optional' && context.type.generics.size() == 1) { 42 | context.parentContext.typeSchemaGenerator.addType(context.scheme, context.type.generics[0], context.inlineRefs, context.references, false) 43 | return true 44 | } 45 | 46 | return false 47 | }) 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/kotlin/io/javalin/openapi/processor/ComponentAnnotationsTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.javalin.openapi.processor 4 | 5 | import io.javalin.openapi.Nullability.NULLABLE 6 | import io.javalin.openapi.OpenApi 7 | import io.javalin.openapi.OpenApiContent 8 | import io.javalin.openapi.OpenApiDescription 9 | import io.javalin.openapi.OpenApiNullable 10 | import io.javalin.openapi.OpenApiPropertyType 11 | import io.javalin.openapi.OpenApiResponse 12 | import io.javalin.openapi.processor.specification.OpenApiAnnotationProcessorSpecification 13 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.json 14 | import net.javacrumbs.jsonunit.assertj.assertThatJson 15 | import org.junit.jupiter.api.Test 16 | import java.math.BigDecimal 17 | 18 | internal class ComponentAnnotationsTest : OpenApiAnnotationProcessorSpecification() { 19 | 20 | @OpenApiDescription("Type description") 21 | private class ClassWithOpenApiDescription( 22 | @get:OpenApiDescription("Property description") 23 | val testProperty: String 24 | ) 25 | 26 | @OpenApi( 27 | path = "/description", 28 | versions = ["should_include_openapi_description"], 29 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = ClassWithOpenApiDescription::class)])] 30 | ) 31 | @Test 32 | fun should_include_openapi_description() = withOpenApi("should_include_openapi_description") { 33 | println(it) 34 | 35 | assertThatJson(it) 36 | .inPath("$.components.schemas.ClassWithOpenApiDescription") 37 | .isObject 38 | .containsEntry("description", "Type description") 39 | 40 | assertThatJson(it) 41 | .inPath("$.components.schemas.ClassWithOpenApiDescription.properties.testProperty") 42 | .isObject 43 | .containsEntry("description", "Property description") 44 | } 45 | 46 | private class ClassWithOpenApiType( 47 | @get:OpenApiPropertyType(definedBy = Double::class, nullability = NULLABLE) 48 | val testProperty: BigDecimal? 49 | ) 50 | 51 | @OpenApi( 52 | path = "/type", 53 | versions = ["should_change_property_type"], 54 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = ClassWithOpenApiType::class)])] 55 | ) 56 | @Test 57 | fun should_change_property_type() = withOpenApi("should_change_property_type") { 58 | println(it) 59 | 60 | assertThatJson(it) 61 | .inPath("$.components.schemas.ClassWithOpenApiType") 62 | .isObject 63 | .isEqualTo(json(""" 64 | { 65 | "type": "object", 66 | "additionalProperties": false, 67 | "properties": { 68 | "testProperty": { 69 | "type": "number", 70 | "nullable": true, 71 | "format": "double" 72 | } 73 | } 74 | } 75 | """)) 76 | } 77 | 78 | private class ClassWithNullableProperties( 79 | @get:OpenApiNullable 80 | val testProperty: String, 81 | @get:OpenApiNullable(nullable = false) 82 | val optionalProperty: String?, 83 | ) 84 | 85 | @OpenApi( 86 | path = "/nullability", 87 | versions = ["should_control_nullability"], 88 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = ClassWithNullableProperties::class)])] 89 | ) 90 | @Test 91 | fun should_add_nullable_property() = withOpenApi("should_control_nullability") { 92 | println(it) 93 | 94 | assertThatJson(it) 95 | .inPath("$.components.schemas.ClassWithNullableProperties.properties.testProperty") 96 | .isObject 97 | .containsEntry("nullable", true) 98 | 99 | assertThatJson(it) 100 | .inPath("$.components.schemas.ClassWithNullableProperties.properties.optionalProperty") 101 | .isObject 102 | .containsEntry("nullable", false) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/kotlin/io/javalin/openapi/processor/CompositionTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.javalin.openapi.processor 4 | 5 | import io.javalin.openapi.Custom 6 | import io.javalin.openapi.Discriminator 7 | import io.javalin.openapi.DiscriminatorMappingName 8 | import io.javalin.openapi.DiscriminatorProperty 9 | import io.javalin.openapi.JsonSchema 10 | import io.javalin.openapi.OneOf 11 | import io.javalin.openapi.OpenApi 12 | import io.javalin.openapi.OpenApiContent 13 | import io.javalin.openapi.OpenApiName 14 | import io.javalin.openapi.OpenApiResponse 15 | import io.javalin.openapi.processor.specification.OpenApiAnnotationProcessorSpecification 16 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson 17 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.json 18 | import org.junit.jupiter.api.Test 19 | 20 | internal class CompositionTest : OpenApiAnnotationProcessorSpecification() { 21 | 22 | interface Storage 23 | 24 | class FileSystemStorage( 25 | @get:Custom(name = "const", "fs") 26 | val type: String = "fs" 27 | ) : Storage 28 | 29 | class S3Storage( 30 | @get:Custom(name = "const", "s3") 31 | val type: String = "s3" 32 | ) : Storage 33 | 34 | @JsonSchema 35 | class SomeConfiguration( 36 | @get:OneOf(FileSystemStorage::class, S3Storage::class) 37 | val storage: Storage 38 | ) 39 | 40 | @Test 41 | fun should_generate_valid_json_scheme_with_composition_property() = withJsonScheme(SomeConfiguration::class.java.canonicalName) { 42 | assertThatJson(it) 43 | .inPath("properties.storage") 44 | .isObject 45 | .isEqualTo(json(""" 46 | { 47 | "oneOf": [ 48 | { 49 | "type": "object", 50 | "additionalProperties": false, 51 | "properties": { 52 | "type": { 53 | "type": "string", 54 | "const": "fs" 55 | } 56 | }, 57 | "required": [ 58 | "type" 59 | ] 60 | }, 61 | { 62 | "type": "object", 63 | "additionalProperties": false, 64 | "properties": { 65 | "type": { 66 | "type": "string", 67 | "const": "s3" 68 | } 69 | }, 70 | "required": [ 71 | "type" 72 | ] 73 | } 74 | ] 75 | } 76 | """)) 77 | } 78 | 79 | @OneOf( 80 | discriminator = Discriminator( 81 | property = DiscriminatorProperty( 82 | name = "type", 83 | type = String::class, 84 | injectInMappings = true 85 | ) 86 | ) 87 | ) 88 | sealed interface Union 89 | 90 | @DiscriminatorMappingName("class-a") 91 | data class A(val a: Int) : Union 92 | 93 | @DiscriminatorMappingName("class-b") 94 | @OpenApiName("B") 95 | data class C(val b: String) : Union 96 | 97 | @OpenApi( 98 | path = "discriminator", 99 | versions = ["should_resolve_subtypes_as_mapping"], 100 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = Union::class)])] 101 | ) 102 | @Test 103 | fun should_resolve_subtypes_as_mapping() = withOpenApi("should_resolve_subtypes_as_mapping") { 104 | println(it) 105 | 106 | assertThatJson(it) 107 | .inPath("$.components.schemas.Union") 108 | .isObject 109 | .isEqualTo(json(""" 110 | { 111 | "oneOf": [ 112 | { 113 | "${'$'}ref": "#/components/schemas/A" 114 | }, 115 | { 116 | "${'$'}ref": "#/components/schemas/B" 117 | } 118 | ], 119 | "discriminator": { 120 | "propertyName": "type", 121 | "mapping": { 122 | "class-a": "#/components/schemas/A", 123 | "class-b": "#/components/schemas/B" 124 | } 125 | } 126 | } 127 | """)) 128 | 129 | assertThatJson(it) 130 | .inPath("$.components.schemas.A") 131 | .isObject 132 | .isEqualTo(json(""" 133 | { 134 | "type": "object", 135 | "additionalProperties": false, 136 | "properties": { 137 | "a": { 138 | "type": "integer", 139 | "format": "int32" 140 | }, 141 | "type": { 142 | "type": "string" 143 | } 144 | }, 145 | "required": [ 146 | "a", 147 | "type" 148 | ] 149 | } 150 | """)) 151 | 152 | assertThatJson(it) 153 | .inPath("$.components.schemas.B") 154 | .isObject 155 | .isEqualTo(json(""" 156 | { 157 | "type": "object", 158 | "additionalProperties": false, 159 | "properties": { 160 | "b": { 161 | "type": "string" 162 | }, 163 | "type": { 164 | "type": "string" 165 | } 166 | }, 167 | "required": [ 168 | "b", 169 | "type" 170 | ] 171 | } 172 | """)) 173 | } 174 | 175 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/kotlin/io/javalin/openapi/processor/CustomAnnotationsTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.javalin.openapi.processor 4 | 5 | import io.javalin.openapi.Custom 6 | import io.javalin.openapi.CustomAnnotation 7 | import io.javalin.openapi.HttpMethod.GET 8 | import io.javalin.openapi.OpenApi 9 | import io.javalin.openapi.OpenApiContent 10 | import io.javalin.openapi.OpenApiName 11 | import io.javalin.openapi.OpenApiResponse 12 | import io.javalin.openapi.processor.specification.OpenApiAnnotationProcessorSpecification 13 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.json 14 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.value 15 | import net.javacrumbs.jsonunit.assertj.assertThatJson 16 | import org.junit.jupiter.api.Test 17 | import kotlin.annotation.AnnotationTarget.CLASS 18 | import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER 19 | 20 | internal class CustomAnnotationsTest : OpenApiAnnotationProcessorSpecification() { 21 | 22 | @CustomAnnotation 23 | @Target(CLASS) 24 | private annotation class CustomAnnotationOnClass(val onClass: BooleanArray) 25 | 26 | @CustomAnnotation 27 | @Target(PROPERTY_GETTER) 28 | private annotation class CustomAnnotationOnGetter(val onGetter: BooleanArray) 29 | 30 | @Custom(name = "description", value = "Custom description") 31 | @CustomAnnotationOnClass(onClass = [true]) 32 | private class CustomEntity( 33 | @get:CustomAnnotationOnGetter(onGetter = [true]) 34 | val element: Map> 35 | ) 36 | 37 | @OpenApi( 38 | path = "/custom", 39 | versions = ["should_include_custom_annotation_in_type_scheme"], 40 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = CustomEntity::class)])] 41 | ) 42 | @Test 43 | fun should_include_custom_annotation_in_type_scheme() = withOpenApi("should_include_custom_annotation_in_type_scheme") { 44 | println(it) 45 | 46 | assertThatJson(it) 47 | .inPath("$.paths['/custom'].get.responses.200.content['application/json'].schema") 48 | .isObject 49 | .containsEntry("\$ref", "#/components/schemas/CustomEntity") 50 | 51 | assertThatJson(it) 52 | .inPath("$.components.schemas.CustomEntity") 53 | .isObject 54 | .containsEntry("onClass", json("[true]")) 55 | .containsEntry("description", "Custom description") 56 | 57 | assertThatJson(it) 58 | .inPath("$.components.schemas.CustomEntity.properties.element") 59 | .isObject 60 | .containsEntry("onGetter", json("[true]")) 61 | } 62 | 63 | @OpenApiName("PandaEntity") 64 | private class OpenApiNameEntity 65 | 66 | @OpenApi( 67 | path = "name", 68 | methods = [GET], 69 | versions = ["should_rename_entity"], 70 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = OpenApiNameEntity::class)])] 71 | ) 72 | @Test 73 | fun should_rename_entity() = withOpenApi("should_rename_entity") { 74 | assertThatJson(it) 75 | .inPath("$.paths['/name'].get.responses.200.content['application/json'].schema") 76 | .isObject 77 | .containsEntry("\$ref", "#/components/schemas/PandaEntity") 78 | 79 | assertThatJson(it) 80 | .inPath("$.components.schemas.PandaEntity") 81 | .isObject 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/kotlin/io/javalin/openapi/processor/CustomTypeMappingsTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.javalin.openapi.processor 4 | 5 | import io.javalin.openapi.OpenApi 6 | import io.javalin.openapi.OpenApiContent 7 | import io.javalin.openapi.OpenApiResponse 8 | import io.javalin.openapi.processor.specification.OpenApiAnnotationProcessorSpecification 9 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.json 10 | import net.javacrumbs.jsonunit.assertj.assertThatJson 11 | import org.junit.jupiter.api.Test 12 | import java.util.Optional 13 | 14 | internal class CustomTypeMappingsTest : OpenApiAnnotationProcessorSpecification() { 15 | 16 | class EntityWithOptional( 17 | val text: Optional // it will be mapped by `compile/openapi.groovy` script 18 | ) 19 | 20 | @OpenApi( 21 | path = "/optional", 22 | versions = ["should_map_optional_using_custom_mapping"], 23 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = EntityWithOptional::class)])] 24 | ) 25 | @Test 26 | fun should_map_optional_using_custom_mapping() = withOpenApi("should_map_optional_using_custom_mapping") { 27 | println(it) 28 | 29 | assertThatJson(it) 30 | .inPath("$.components.schemas.EntityWithOptional") 31 | .isObject 32 | .isEqualTo(json(""" 33 | { 34 | "type": "object", 35 | "additionalProperties": false, 36 | "properties": { 37 | "text": { 38 | "type": "string" 39 | } 40 | }, 41 | "required": [ 42 | "text" 43 | ] 44 | } 45 | } 46 | """)) 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/kotlin/io/javalin/openapi/processor/OpenApiAnnotationTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.javalin.openapi.processor 4 | 5 | import io.javalin.openapi.HttpMethod 6 | import io.javalin.openapi.OpenApi 7 | import io.javalin.openapi.OpenApiCallback 8 | import io.javalin.openapi.OpenApiContent 9 | import io.javalin.openapi.OpenApiRequestBody 10 | import io.javalin.openapi.OpenApiResponse 11 | import io.javalin.openapi.processor.specification.OpenApiAnnotationProcessorSpecification 12 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.json 13 | import net.javacrumbs.jsonunit.assertj.assertThatJson 14 | import org.junit.jupiter.api.Test 15 | 16 | internal class OpenApiAnnotationTest : OpenApiAnnotationProcessorSpecification() { 17 | 18 | @OpenApi( 19 | path = "/", 20 | versions = ["should_generate_info"] 21 | ) 22 | @Test 23 | fun should_generate_info() = withOpenApi("should_generate_info") { 24 | assertThatJson(it) 25 | .isObject 26 | .containsEntry("openapi", "3.0.3") 27 | .containsEntry("info", json("""{ "title":"", "version": "" }""")) 28 | } 29 | 30 | @OpenApi( 31 | path = "/basic", 32 | versions = ["should_contain_all_basic_properties_from_openapi_annotation"], 33 | summary = "Test summary", 34 | operationId = "Test operation id", 35 | description = "Test description", 36 | tags = ["Test tag"], 37 | deprecated = true, 38 | ) 39 | @Test 40 | fun should_contain_all_basic_properties_from_openapi_annotation() = withOpenApi("should_contain_all_basic_properties_from_openapi_annotation") { 41 | assertThatJson(it) 42 | .inPath("$.paths['/basic'].get") 43 | .isObject 44 | .containsAllEntriesOf(linkedMapOf( 45 | "tags" to json("['Test tag']"), 46 | "summary" to "Test summary", 47 | "description" to "Test description", 48 | "operationId" to "Test operation id", 49 | "parameters" to json("[]"), 50 | "deprecated" to true, 51 | "security" to json("[]") 52 | )) 53 | } 54 | 55 | @OpenApi( 56 | path = "/callback", 57 | versions = ["should_generate_callback"], 58 | callbacks = [ 59 | OpenApiCallback( 60 | name = "onData", 61 | url = "{${'$'}request.body#/url}/callback", 62 | method = HttpMethod.POST, 63 | summary = "Test summary", 64 | description = "Test description", 65 | requestBody = OpenApiRequestBody( 66 | content = [OpenApiContent(from = String::class)] 67 | ), 68 | responses = [ 69 | OpenApiResponse(status = "200", content = [OpenApiContent(from = String::class)]) 70 | ] 71 | ) 72 | ] 73 | ) 74 | @Test 75 | fun should_generate_callback() = withOpenApi("should_generate_callback") { 76 | println(it) 77 | 78 | assertThatJson(it) 79 | .inPath("$.paths['/callback'].get.callbacks") 80 | .isObject 81 | .isEqualTo(json("""{ 82 | "onData": { 83 | "{${'$'}request.body#/url}/callback": { 84 | "post": { 85 | "summary": "Test summary", 86 | "description": "Test description", 87 | "requestBody": { 88 | "content": { 89 | "text/plain": { 90 | "schema": { 91 | "type": "string" 92 | } 93 | } 94 | }, 95 | required: false 96 | }, 97 | "responses": { 98 | "200": { 99 | "description": "OK", 100 | "content": { 101 | "text/plain": { 102 | "schema": { 103 | "type": "string" 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | }""")) 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/kotlin/io/javalin/openapi/processor/SchemeTest.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.processor 2 | 3 | import io.javalin.openapi.ContentType 4 | import io.javalin.openapi.OpenApi 5 | import io.javalin.openapi.OpenApiContent 6 | import io.javalin.openapi.OpenApiResponse 7 | import io.javalin.openapi.processor.specification.OpenApiAnnotationProcessorSpecification 8 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.json 9 | import net.javacrumbs.jsonunit.assertj.assertThatJson 10 | import org.junit.jupiter.api.Test 11 | import java.io.Serializable 12 | 13 | internal class SchemeTest : OpenApiAnnotationProcessorSpecification() { 14 | 15 | private open class BaseType { 16 | val baseProperty: String = "Test" 17 | val baseNested: BaseType.NestedClass = NestedClass() 18 | 19 | private inner class NestedClass { 20 | val nestedProperty: String = "Test" 21 | } 22 | } 23 | 24 | private class FinalClass : BaseType(), Serializable { 25 | val finalProperty: String = "Test" 26 | val finalNested: FinalClass.NestedClass = NestedClass() 27 | 28 | private inner class NestedClass { 29 | val nestedProperty: String = "Test" 30 | } 31 | } 32 | 33 | @OpenApi( 34 | path = "content-types", 35 | versions = ["should_generate_reference_with_inherited_properties"], 36 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = FinalClass::class, type = ContentType.JSON)])] 37 | ) 38 | @Test 39 | fun should_generate_reference_with_inherited_properties() = withOpenApi("should_generate_reference_with_inherited_properties") { 40 | println(it) 41 | 42 | assertThatJson(it) 43 | .inPath("$.components.schemas.FinalClass.properties") 44 | .isObject 45 | .isEqualTo(json(""" 46 | { 47 | "baseProperty": { 48 | "type": "string" 49 | }, 50 | "baseNested": { 51 | "${'$'}ref": "#/components/schemas/NestedClass" 52 | }, 53 | "finalProperty": { 54 | "type": "string" 55 | }, 56 | "finalNested": { 57 | "${'$'}ref": "#/components/schemas/NestedClass" 58 | } 59 | } 60 | """)) 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/kotlin/io/javalin/openapi/processor/TypeMappersTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.javalin.openapi.processor 4 | 5 | import io.javalin.openapi.OpenApi 6 | import io.javalin.openapi.OpenApiContent 7 | import io.javalin.openapi.OpenApiResponse 8 | import io.javalin.openapi.processor.specification.OpenApiAnnotationProcessorSpecification 9 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.json 10 | import net.javacrumbs.jsonunit.assertj.assertThatJson 11 | import org.bson.types.ObjectId 12 | import org.junit.jupiter.api.Test 13 | import java.io.File 14 | import java.io.InputStream 15 | import java.math.BigDecimal 16 | import java.time.Instant 17 | import java.time.LocalDate 18 | import java.time.LocalDateTime 19 | import java.util.Date 20 | import java.util.UUID 21 | 22 | internal class TypeMappersTest : OpenApiAnnotationProcessorSpecification() { 23 | 24 | class CustomType // mapped by openapi.groovy 25 | 26 | @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "RemoveRedundantQualifierName") 27 | class SimpleTypesList( 28 | val customType: CustomType, 29 | val boolean: Boolean, 30 | val booleanObject: java.lang.Boolean, 31 | val byte: Byte, 32 | val byteObject: java.lang.Byte, 33 | val short: Short, 34 | val shortObject: java.lang.Short, 35 | val int: Int, 36 | val intObject: java.lang.Integer, 37 | val long: Long, 38 | val longObject: java.lang.Long, 39 | val float: Float, 40 | val floatObject: java.lang.Float, 41 | val double: Double, 42 | val doubleObject: java.lang.Double, 43 | val char: Char, 44 | val charObject: java.lang.Character, 45 | val string: String, 46 | val bigDecimal: BigDecimal, 47 | val uuid: UUID, 48 | val objectId: ObjectId, 49 | val byteArray: ByteArray, 50 | val inputStream: InputStream, 51 | val file: File, 52 | val date: Date, 53 | val localDate: LocalDate, 54 | val localDateTime: LocalDateTime, 55 | val instant: Instant, 56 | val obj: Object, 57 | val map: Map<*, *>, 58 | val mapWithList: Map<*, List<*>> 59 | ) 60 | 61 | @OpenApi( 62 | path = "simple-types", 63 | versions = ["should_map_all_simple_types"], 64 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = SimpleTypesList::class)])] 65 | ) 66 | @Test 67 | fun should_map_all_simple_types() = withOpenApi("should_map_all_simple_types") { 68 | println(it) 69 | 70 | assertThatJson(it) 71 | .inPath("$.components.schemas.SimpleTypesList.properties") 72 | .isObject 73 | .isEqualTo(json(""" 74 | { 75 | "customType": { 76 | "type": "string" 77 | }, 78 | "boolean": { 79 | "type": "boolean" 80 | }, 81 | "booleanObject": { 82 | "type": "boolean" 83 | }, 84 | "byte": { 85 | "type": "integer", 86 | "format": "int32" 87 | }, 88 | "byteObject": { 89 | "type": "integer", 90 | "format": "int32" 91 | }, 92 | "short": { 93 | "type": "integer", 94 | "format": "int32" 95 | }, 96 | "shortObject": { 97 | "type": "integer", 98 | "format": "int32" 99 | }, 100 | "int": { 101 | "type": "integer", 102 | "format": "int32" 103 | }, 104 | "intObject": { 105 | "type": "integer", 106 | "format": "int32" 107 | }, 108 | "long": { 109 | "type": "integer", 110 | "format": "int64" 111 | }, 112 | "longObject": { 113 | "type": "integer", 114 | "format": "int64" 115 | }, 116 | "float": { 117 | "type": "number", 118 | "format": "float" 119 | }, 120 | "floatObject": { 121 | "type": "number", 122 | "format": "float" 123 | }, 124 | "double": { 125 | "type": "number", 126 | "format": "double" 127 | }, 128 | "doubleObject": { 129 | "type": "number", 130 | "format": "double" 131 | }, 132 | "char": { 133 | "type": "string" 134 | }, 135 | "charObject": { 136 | "type": "string" 137 | }, 138 | "string": { 139 | "type": "string" 140 | }, 141 | "bigDecimal": { 142 | "type": "string" 143 | }, 144 | "uuid": { 145 | "type": "string" 146 | }, 147 | "objectId": { 148 | "type": "string" 149 | }, 150 | "byteArray": { 151 | "type": "string", 152 | "format": "binary" 153 | }, 154 | "inputStream": { 155 | "type": "string", 156 | "format": "binary" 157 | }, 158 | "file": { 159 | "type": "string", 160 | "format": "binary" 161 | }, 162 | "date": { 163 | "type": "string", 164 | "format": "date" 165 | }, 166 | "localDate": { 167 | "type": "string", 168 | "format": "date" 169 | }, 170 | "localDateTime": { 171 | "type": "string", 172 | "format": "date-time" 173 | }, 174 | "instant": { 175 | "type": "string", 176 | "format": "date-time" 177 | }, 178 | "obj": { 179 | "type": "object" 180 | }, 181 | "map": { 182 | "type": "object", 183 | "additionalProperties": { 184 | "type": "object" 185 | } 186 | }, 187 | "mapWithList": { 188 | "type": "object", 189 | "additionalProperties": { 190 | "type": "array", 191 | "items": { 192 | "type": "object" 193 | } 194 | } 195 | } 196 | }""" 197 | )) 198 | } 199 | 200 | private class Loop( 201 | val self: Loop?, 202 | ) 203 | 204 | 205 | @OpenApi( 206 | path = "recursive", 207 | versions = ["should_map_recursive_type"], 208 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = Loop::class)])] 209 | ) 210 | @Test 211 | fun should_map_recursive_type() = withOpenApi("should_map_recursive_type") { 212 | assertThatJson(it) 213 | .inPath("$.components.schemas.Loop.properties.self") 214 | .isObject 215 | .containsEntry("\$ref", "#/components/schemas/Loop") 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/kotlin/io/javalin/openapi/processor/UserCasesTest.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.processor 2 | 3 | import io.javalin.openapi.* 4 | import io.javalin.openapi.processor.specification.OpenApiAnnotationProcessorSpecification 5 | import net.javacrumbs.jsonunit.assertj.JsonAssertions.json 6 | import net.javacrumbs.jsonunit.assertj.assertThatJson 7 | import org.junit.jupiter.api.Test 8 | 9 | internal class UserCasesTest : OpenApiAnnotationProcessorSpecification() { 10 | 11 | /* 12 | * GH-125 Array fields in the custom annotations don't appear in the output 13 | * ~ https://github.com/javalin/javalin-openapi/issues/125 14 | */ 15 | 16 | @Target(AnnotationTarget.PROPERTY_GETTER) 17 | @CustomAnnotation 18 | annotation class Schema( 19 | val allowableValues: Array = [], 20 | val description: String = "", 21 | val example: String = "", 22 | val format: String = "", 23 | val pattern: String = "" 24 | ) 25 | 26 | data class KeypairCreateResponse( 27 | @get:Schema( 28 | allowableValues = ["valid", "expired", "revoked"], 29 | format = "fingerprint", 30 | pattern = "^(valid|expired|revoked)$", 31 | description = "status of the key like valid|expired|revoked", 32 | ) 33 | val fingerprint: String 34 | ) 35 | 36 | @OpenApi( 37 | path = "gh-125", 38 | versions = ["gh-125"], 39 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = KeypairCreateResponse::class)])] 40 | ) 41 | @Test 42 | fun gh125() = withOpenApi("gh-125") { 43 | println(it) 44 | } 45 | 46 | /* 47 | * GH-108 Ignore inherited properties 48 | * ~ https://github.com/javalin/javalin-openapi/issues/108 49 | */ 50 | 51 | interface SpecificRecord { 52 | fun getRecord(): String // it has to be implemented 53 | } 54 | 55 | open class SpecificRecordBase { 56 | fun getRecordBase(): String = "RecordBase" // it'll be excluded 57 | } 58 | 59 | class EmailRequest(val email: String) : SpecificRecordBase(), SpecificRecord { 60 | override fun getRecord(): String = "Record" // it will be excluded by `compile/openapi.groovy` script 61 | } 62 | 63 | @OpenApi( 64 | path = "gh-108", 65 | versions = ["gh-108"], 66 | responses = [OpenApiResponse(status = "200", content = [OpenApiContent(from = EmailRequest::class)])] 67 | ) 68 | @Test 69 | fun gh108() = withOpenApi("gh-108") { 70 | assertThatJson(it) 71 | .inPath("$.components.schemas.EmailRequest") 72 | .isObject 73 | .containsEntry("required", json("['email']")) 74 | } 75 | 76 | /* 77 | * GH-151 Support auto-generated operationId like in old OpenApi plugin 78 | * ~ https://github.com/javalin/javalin-openapi/issues/151 79 | */ 80 | 81 | @OpenApi( 82 | path = "/api/panda/list", 83 | operationId = OpenApiOperation.AUTO_GENERATE, 84 | versions = ["should_generate_operation_id_from_path"] 85 | ) 86 | @Test 87 | fun should_generate_operation_id_from_path() = withOpenApi("should_generate_operation_id_from_path") { 88 | println(it) 89 | 90 | assertThatJson(it) 91 | .inPath("$.paths['/api/panda/list'].get.operationId") 92 | .isString 93 | .isEqualTo("getApiPandaList") 94 | } 95 | 96 | @OpenApi( 97 | path = "/api/panda/{pandaId}/name/", 98 | operationId = OpenApiOperation.AUTO_GENERATE, 99 | versions = ["should_generate_operation_id_from_path_with_parameters"] 100 | ) 101 | @Test 102 | fun should_generate_operation_id_from_path_with_parameters() = withOpenApi("should_generate_operation_id_from_path_with_parameters"){ 103 | println(it) 104 | 105 | assertThatJson(it) 106 | .inPath("$.paths['/api/panda/{pandaId}/name/'].get.operationId") 107 | .isString 108 | .isEqualTo("getApiPandaByPandaIdNameByStartsWith") 109 | } 110 | 111 | @OpenApi( 112 | path = "/api/cat/{cat-id}", 113 | operationId = OpenApiOperation.AUTO_GENERATE, 114 | versions = ["should_generate_operation_id_from_path_with_parameters_hyphenated"] 115 | ) 116 | @Test 117 | fun should_generate_operation_id_from_path_with_parameters_hyphenated() = withOpenApi("should_generate_operation_id_from_path_with_parameters_hyphenated"){ 118 | println(it) 119 | // TODO not sure what to expect here 120 | assertThatJson(it) 121 | .inPath("$.paths['/api/cat/{cat-id}'].get.operationId") 122 | .isString 123 | .isEqualTo("getApiCatByCatId") 124 | } 125 | 126 | @OpenApi( 127 | path = "/api/panda", 128 | methods= [HttpMethod.PUT], 129 | operationId = OpenApiOperation.AUTO_GENERATE, 130 | versions = ["should_generate_operation_id_from_path_method_put"] 131 | ) 132 | @Test 133 | fun should_generate_operation_id_from_path_method_put() = withOpenApi("should_generate_operation_id_from_path_method_put") { 134 | println(it) 135 | 136 | assertThatJson(it) 137 | .inPath("$.paths['/api/panda'].put.operationId") 138 | .isString 139 | .isEqualTo("putApiPanda") 140 | } 141 | 142 | @OpenApi( 143 | path = "/vip-accounts/{vip-account-id}", 144 | operationId = OpenApiOperation.AUTO_GENERATE, 145 | versions = ["should_generate_operation_id_from_hyphenated_path_with_parameters_hyphenated"] 146 | ) 147 | @Test 148 | fun should_generate_operation_id_from_hyphenated_path_with_parameters_hyphenated() = withOpenApi("should_generate_operation_id_from_hyphenated_path_with_parameters_hyphenated"){ 149 | println(it) 150 | 151 | assertThatJson(it) 152 | .inPath("$.paths['/vip-accounts/{vip-account-id}'].get.operationId") 153 | .isString 154 | .isEqualTo("getVipAccountsByVipAccountId") 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/kotlin/io/javalin/openapi/processor/specification/OpenApiAnnotationProcessorSpecification.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.processor.specification 2 | 3 | import io.javalin.openapi.JsonSchemaLoader 4 | import io.javalin.openapi.OpenApiLoader 5 | import org.junit.jupiter.api.Assertions 6 | 7 | typealias SchemeConsumer = (String) -> Unit 8 | 9 | internal abstract class OpenApiAnnotationProcessorSpecification { 10 | 11 | private companion object { 12 | val openApiSchemes = OpenApiLoader().loadOpenApiSchemes() 13 | val jsonSchemes = JsonSchemaLoader().loadGeneratedSchemes() 14 | } 15 | 16 | 17 | fun withOpenApi(name: String, consumer: SchemeConsumer) { 18 | openApiSchemes[name.replace(" ", "-")] 19 | ?.also { consumer(it) } 20 | ?: failWithSchemeNotFound(name) 21 | } 22 | 23 | fun withJsonScheme(name: String, consumer: SchemeConsumer) { 24 | jsonSchemes 25 | .find { it.name.contains(name) } 26 | ?.also { consumer(it.getContentAsString()) } 27 | ?: failWithSchemeNotFound(name) 28 | } 29 | 30 | private fun failWithSchemeNotFound(name: String) { 31 | Assertions.fail("Scheme '$name' not found") 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | io.javalin.openapi.processor.OpenApiAnnotationProcessor -------------------------------------------------------------------------------- /openapi-annotation-processor/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /openapi-specification/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Javalin OpenAPI Specification | Compile-time OpenAPI integration for Javalin 6.x" 2 | 3 | dependencies { 4 | val jacksonVersion = "2.18.1" 5 | api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") 6 | api("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") 7 | api("com.google.code.gson:gson:2.10.1") 8 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/Info.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi 2 | 3 | import java.util.function.Consumer 4 | 5 | /** https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#infoObject */ 6 | class OpenApiInfo { 7 | /** REQUIRED. The title of the API */ 8 | var title: String? = null 9 | fun title(title: String) = apply { this.title = title } 10 | 11 | /** A short summary of the API */ 12 | var summary: String? = null 13 | fun summary(summary: String) = apply { this.summary = summary } 14 | 15 | /** A description of the API. CommonMark's syntax MAY be used for rich text representation */ 16 | var description: String? = null 17 | fun description(description: String) = apply { this.description = description } 18 | 19 | /** A URL to the Terms of Service for the API. This MUST be in the form of a URL */ 20 | var termsOfService: String? = null 21 | fun termsOfService(termsOfService: String) = apply { this.termsOfService = termsOfService } 22 | 23 | /** The contact information for the exposed API */ 24 | var contact: OpenApiContact? = null 25 | @JvmOverloads 26 | fun contact(name: String?, url: String? = null, email: String? = null) = withContact { it.name(name).url(url).email(email) } 27 | fun withContact(contact: Consumer) = apply { this.contact = OpenApiContact().apply { contact.accept(this) } } 28 | 29 | /** The license information for the exposed API */ 30 | var license: OpenApiLicense? = null 31 | @JvmOverloads 32 | fun license(name: String?, url: String? = null, identifier: String? = null) = withLicense { it.name(name).url(url).identifier(identifier) } 33 | fun withLicense(license: Consumer) = apply { this.license = OpenApiLicense().apply { license.accept(this) } } 34 | 35 | /** REQUIRED. The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API implementation version). */ 36 | var version: String? = null 37 | fun version(version: String) = apply { this.version = version } 38 | } 39 | 40 | /** https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#contactObject */ 41 | class OpenApiContact { 42 | /** The identifying name of the contact person/organization. */ 43 | var name: String? = null 44 | fun name(name: String?) = apply { this.name = name } 45 | 46 | /** The URL pointing to the contact information. This MUST be in the form of a URL. */ 47 | var url: String? = null 48 | fun url(url: String?) = apply { this.url = url } 49 | 50 | /** The email address of the contact person/organization. This MUST be in the form of an email address. */ 51 | var email: String? = null 52 | fun email(email: String?) = apply { this.email = email } 53 | } 54 | 55 | /** https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#licenseObject */ 56 | class OpenApiLicense { 57 | /** REQUIRED. The license name used for the API */ 58 | var name: String? = null 59 | fun name(name: String?) = apply { this.name = name } 60 | 61 | /** An SPDX license expression for the API. The identifier field is mutually exclusive of the url field. */ 62 | var identifier: String? = null 63 | fun identifier(identifier: String?) = apply { this.identifier = identifier } 64 | 65 | /** A URL to the license used for the API. This MUST be in the form of a URL. The url field is mutually exclusive of the identifier field */ 66 | var url: String? = null 67 | fun url(url: String?) = apply { this.url = url } 68 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/JsonSchemaAnnotations.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi 2 | 3 | import java.io.InputStream 4 | import java.util.function.Supplier 5 | import kotlin.annotation.AnnotationRetention.RUNTIME 6 | import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS 7 | import kotlin.annotation.AnnotationTarget.CLASS 8 | import kotlin.annotation.AnnotationTarget.FIELD 9 | import kotlin.annotation.AnnotationTarget.FUNCTION 10 | import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER 11 | import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER 12 | import kotlin.reflect.KClass 13 | 14 | @Target(CLASS) 15 | @Retention(RUNTIME) 16 | annotation class JsonSchema( 17 | /** 18 | * By default, each usage of @JsonSchema annotation results in generated `/json-schemas/{type qualifier}` resource file. 19 | * If for some reason you need to use @JsonSchema in your OpenAPI specification, you can disable this behaviour. 20 | */ 21 | val generateResource: Boolean = true, 22 | /** 23 | * By default, all non fields are marked as required. 24 | * You can disable this behaviour for given type using this property 25 | */ 26 | val requireNonNulls: Boolean = true 27 | ) 28 | 29 | enum class Composition(val propertyName: String, val type: KClass<*>) { 30 | ONE_OF("oneOf", OneOf::class), 31 | ANY_OF("anyOf", AnyOf::class), 32 | ALL_OF("allOf", AllOf::class) 33 | } 34 | 35 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, FIELD, CLASS) 36 | @Retention(RUNTIME) 37 | annotation class OneOf( 38 | /** List of associated classes to list */ 39 | vararg val value: KClass<*>, 40 | /** Define discriminator object */ 41 | val discriminator: Discriminator = Discriminator() 42 | ) 43 | 44 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, FIELD, CLASS) 45 | @Retention(RUNTIME) 46 | annotation class AnyOf( 47 | /** List of associated classes to list */ 48 | vararg val value: KClass<*>, 49 | /** Define discriminator object */ 50 | val discriminator: Discriminator = Discriminator() 51 | ) 52 | 53 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, FIELD, CLASS) 54 | @Retention(RUNTIME) 55 | annotation class AllOf( 56 | /** List of associated classes to list */ 57 | vararg val value: KClass<*>, 58 | /** Define discriminator object */ 59 | val discriminator: Discriminator = Discriminator() 60 | ) 61 | 62 | @Retention(RUNTIME) 63 | annotation class Discriminator( 64 | val property: DiscriminatorProperty = DiscriminatorProperty(), 65 | val mapping: Array = [] 66 | ) 67 | 68 | @Retention(RUNTIME) 69 | annotation class DiscriminatorProperty( 70 | val name: String = NULL_STRING, 71 | val type: KClass<*> = NULL_CLASS::class, 72 | val injectInMappings: Boolean = false 73 | ) 74 | 75 | @Retention(RUNTIME) 76 | annotation class MappedClass( 77 | val value: KClass<*>, 78 | val name: String 79 | ) 80 | 81 | @Target(CLASS) 82 | @Retention(RUNTIME) 83 | annotation class DiscriminatorMappingName( 84 | val value: String 85 | ) 86 | 87 | /** Allows you to add custom properties to your schemes */ 88 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, FIELD, CLASS) 89 | @Retention(RUNTIME) 90 | @Repeatable 91 | annotation class Custom( 92 | /* Define name of key for custom property */ 93 | val name: String, 94 | /* Define value of custom property */ 95 | val value: String 96 | ) 97 | 98 | /** Allows you to create custom annotations for a group of custom properties */ 99 | @Target(ANNOTATION_CLASS) 100 | @Retention(RUNTIME) 101 | annotation class CustomAnnotation 102 | 103 | /** Represents resource file in `/json-schemes` directory. */ 104 | data class JsonSchemaResource( 105 | /** The name of resource file. */ 106 | val name: String, 107 | private val content: Supplier 108 | ) { 109 | 110 | /** Returns input stream to the associated resource file. */ 111 | fun getContent(): InputStream = 112 | content.get() 113 | 114 | /** Reads [#getContent] as string */ 115 | fun getContentAsString(): String = 116 | getContent().reader().readText() 117 | 118 | } 119 | 120 | class JsonSchemaLoader { 121 | 122 | fun loadGeneratedSchemes(): Set = 123 | JsonSchemaLoader::class.java.getResourceAsStream("/json-schemes/index") 124 | ?.readAllBytes() 125 | ?.decodeToString() 126 | ?.trim() 127 | ?.split("\n") 128 | ?.asSequence() 129 | ?.distinct() 130 | ?.filter { it.isNotEmpty() } 131 | ?.map { JsonSchemaResource(it) { JsonSchemaLoader::class.java.getResourceAsStream("/json-schemes/$it")!! } } 132 | ?.toSet() 133 | ?: emptySet() 134 | 135 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/OpenApiAnnotations.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal copy of https://github.com/tipsy/javalin/blob/master/javalin/src/main/java/io/javalin/plugin/openapi/annotations/AnnotationApi.kt file. 3 | * In the future it might be replaced with a better impl. 4 | */ 5 | 6 | package io.javalin.openapi 7 | 8 | import io.javalin.openapi.HttpMethod.GET 9 | import io.javalin.openapi.Visibility.PUBLIC 10 | import java.lang.annotation.Repeatable 11 | import kotlin.annotation.AnnotationRetention.RUNTIME 12 | import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS 13 | import kotlin.annotation.AnnotationTarget.CLASS 14 | import kotlin.annotation.AnnotationTarget.FIELD 15 | import kotlin.annotation.AnnotationTarget.FUNCTION 16 | import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER 17 | import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER 18 | import kotlin.reflect.KClass 19 | 20 | /** 21 | * Provide metadata for the generation of the open api documentation to the annotated Handler. 22 | * Source: [Specification](https://swagger.io/specification/) 23 | */ 24 | @Repeatable(value = OpenApis::class) 25 | @Target(CLASS, FIELD, FUNCTION) 26 | @Retention(RUNTIME) 27 | annotation class OpenApi( 28 | /** The described path */ 29 | val path: String, 30 | /** List of methods to describe **/ 31 | val methods: Array = [GET], 32 | /** Schema version **/ 33 | val versions: Array = ["default"], 34 | /** Ignore the endpoint in the open api documentation */ 35 | val ignore: Boolean = false, 36 | /** An optional, string summary, intended to apply to all operations in this path. **/ 37 | val summary: String = NULL_STRING, 38 | /** An optional, string description, intended to apply to all operations in this path. **/ 39 | val description: String = NULL_STRING, 40 | /** 41 | * Unique string used to identify the operation. 42 | * The id MUST be unique among all operations described in the API. 43 | * The operationId value is case-sensitive. 44 | * 45 | * You can also use [OpenApiOperation.AUTO_GENERATE] 46 | * if you want to generate the operationId automatically using the method name. 47 | **/ 48 | val operationId: String = NULL_STRING, 49 | /** Declares this operation to be deprecated. Consumers SHOULD refrain from usage of the declared operation. **/ 50 | val deprecated: Boolean = false, 51 | /** 52 | * A list of tags for API documentation control. 53 | * Tags can be used for logical grouping of operations by resources or any other qualifier. 54 | **/ 55 | val tags: Array = [], 56 | /** Describes applicable cookies */ 57 | val cookies: Array = [], 58 | /** Describes applicable headers */ 59 | val headers: Array = [], 60 | /** Describes applicable path parameters */ 61 | val pathParams: Array = [], 62 | /** Describes applicable query parameters */ 63 | val queryParams: Array = [], 64 | /** Describes applicable form parameters */ 65 | val formParams: Array = [], 66 | /** 67 | * The request body applicable for this operation. 68 | * The requestBody is only supported in HTTP methods where the HTTP 1.1 specification RFC7231 has explicitly defined semantics for request bodies. 69 | * In other cases where the HTTP spec is vague, requestBody SHALL be ignored by consumers. 70 | */ 71 | val requestBody: OpenApiRequestBody = OpenApiRequestBody([]), 72 | /** Describes applicable callbacks */ 73 | val callbacks: Array = [], 74 | // val composedRequestBody: OpenApiComposedRequestBody = OpenApiComposedRequestBody([]), ? 75 | /** The list of possible responses as they are returned from executing this operation. */ 76 | val responses: Array = [], 77 | /** A declaration of which security mechanisms can be used for this operation. */ 78 | val security: Array = [], 79 | ) 80 | 81 | fun OpenApi.getFormattedPath(): String = 82 | when { 83 | !path.startsWith("/") -> "/$path" 84 | else -> path 85 | } 86 | 87 | /** Utility annotation to aggregate multiple [OpenApi] instances */ 88 | @Target(CLASS, FIELD, FUNCTION) 89 | @Retention(RUNTIME) 90 | annotation class OpenApis( 91 | val value: Array = [] 92 | ) 93 | 94 | @Target() 95 | @Retention(RUNTIME) 96 | annotation class OpenApiResponse( 97 | val status: String, 98 | val content: Array = [], 99 | val description: String = NULL_STRING, 100 | val headers: Array = [], 101 | ) 102 | 103 | @Target() 104 | @Retention(RUNTIME) 105 | annotation class OpenApiParam( 106 | val name: String, 107 | val type: KClass<*> = String::class, 108 | val description: String = NULL_STRING, 109 | val deprecated: Boolean = false, 110 | val required: Boolean = false, 111 | val allowEmptyValue: Boolean = false, 112 | val example: String = "" 113 | ) 114 | 115 | @Target() 116 | @Retention(RUNTIME) 117 | annotation class OpenApiRequestBody( 118 | val content: Array, 119 | val required: Boolean = false, 120 | val description: String = NULL_STRING 121 | ) 122 | 123 | @Target() 124 | @Retention(RUNTIME) 125 | annotation class OpenApiCallback( 126 | val name: String, 127 | val url: String, 128 | val method: HttpMethod, 129 | val summary: String = NULL_STRING, 130 | val description: String = NULL_STRING, 131 | val requestBody: OpenApiRequestBody, 132 | val responses: Array 133 | ) 134 | 135 | @Target() 136 | @Retention(RUNTIME) 137 | annotation class OpenApiContent( 138 | val from: KClass<*> = NULL_CLASS::class, 139 | val mimeType: String = ContentType.AUTODETECT, 140 | val type: String = NULL_STRING, 141 | val format: String = NULL_STRING, 142 | val properties: Array = [], 143 | val example: String = NULL_STRING, 144 | val exampleObjects: Array = [], 145 | ) 146 | 147 | @Target() 148 | @Retention(RUNTIME) 149 | annotation class OpenApiContentProperty( 150 | val from: KClass<*> = NULL_CLASS::class, 151 | val name: String, 152 | val isArray: Boolean = false, 153 | val type: String = NULL_STRING, 154 | val format: String = NULL_STRING 155 | ) 156 | 157 | @Target() 158 | @Retention(RUNTIME) 159 | annotation class OpenApiSecurity( 160 | val name: String, 161 | val scopes: Array = [] 162 | ) 163 | 164 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 165 | @Retention(RUNTIME) 166 | annotation class OpenApiIgnore 167 | 168 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 169 | @Retention(RUNTIME) 170 | annotation class OpenApiRequired 171 | 172 | @Target(CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 173 | @Retention(RUNTIME) 174 | annotation class OpenApiName( 175 | val value: String 176 | ) 177 | 178 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 179 | @Retention(RUNTIME) 180 | annotation class OpenApiExample( 181 | val value: String = NULL_STRING, 182 | val objects: Array = [] 183 | ) 184 | 185 | @Target(ANNOTATION_CLASS) 186 | @Retention(RUNTIME) 187 | annotation class OpenApiExampleProperty( 188 | val name: String = NULL_STRING, 189 | val value: String = NULL_STRING, 190 | val objects: Array = [] 191 | ) 192 | 193 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 194 | @Retention(RUNTIME) 195 | @CustomAnnotation 196 | annotation class OpenApiNullable( 197 | val nullable: Boolean = true 198 | ) 199 | 200 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, CLASS) 201 | @Retention(RUNTIME) 202 | annotation class OpenApiDescription( 203 | val value: String 204 | ) 205 | 206 | enum class Nullability { 207 | NULLABLE, 208 | NOT_NULL, 209 | AUTO 210 | } 211 | 212 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 213 | @Retention(RUNTIME) 214 | annotation class OpenApiNumberValidation( 215 | val minimum: String = NULL_STRING, 216 | val exclusiveMinimum: Boolean = false, 217 | val maximum: String = NULL_STRING, 218 | val exclusiveMaximum: Boolean = false, 219 | val multipleOf: String = NULL_STRING 220 | ) 221 | 222 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 223 | @Retention(RUNTIME) 224 | annotation class OpenApiStringValidation( 225 | val minLength: String = NULL_STRING, 226 | val maxLength: String = NULL_STRING, 227 | val format: String = NULL_STRING, 228 | val pattern: String = NULL_STRING 229 | ) 230 | 231 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 232 | @Retention(RUNTIME) 233 | annotation class OpenApiArrayValidation( 234 | val minItems: String = NULL_STRING, 235 | val maxItems: String = NULL_STRING, 236 | val uniqueItems: Boolean = false 237 | ) 238 | 239 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 240 | @Retention(RUNTIME) 241 | annotation class OpenApiObjectValidation( 242 | val minProperties: String = NULL_STRING, 243 | val maxProperties: String = NULL_STRING, 244 | ) 245 | 246 | @Target(CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) 247 | @Retention(RUNTIME) 248 | annotation class OpenApiPropertyType( 249 | val definedBy: KClass<*>, 250 | val nullability: Nullability = Nullability.AUTO 251 | ) 252 | 253 | enum class Visibility(val priority: Int) { 254 | PUBLIC(4), 255 | DEFAULT(3), 256 | PROTECTED(2), 257 | PRIVATE(1) 258 | } 259 | 260 | @Target(CLASS) 261 | @Retention(RUNTIME) 262 | annotation class OpenApiByFields( 263 | val value: Visibility = PUBLIC 264 | ) 265 | 266 | /** Null class because annotations do not support null values */ 267 | @Suppress("ClassName") 268 | class NULL_CLASS 269 | 270 | /** Null string because annotations do not support null values */ 271 | const val NULL_STRING = "-- This string represents a null value and shouldn't be used --" 272 | 273 | object OpenApiOperation { 274 | /** Value to use for auto-generate operationId */ 275 | const val AUTO_GENERATE = "-- Auto-generate operationId on the fly. If you see this message you are either inspecting via debugger or something went wrong --" 276 | } 277 | 278 | object ContentType { 279 | const val JSON = "application/json" 280 | const val HTML = "text/html" 281 | const val FORM_DATA_URL_ENCODED = "application/x-www-form-urlencoded" 282 | const val FORM_DATA_MULTIPART = "multipart/form-data" 283 | const val AUTODETECT = "AUTODETECT - Will be replaced later" 284 | } 285 | 286 | enum class ComposedType { 287 | NULL, 288 | ANY_OF, 289 | ONE_OF; 290 | } 291 | 292 | enum class HttpMethod { 293 | POST, 294 | GET, 295 | PUT, 296 | PATCH, 297 | DELETE, 298 | HEAD, 299 | OPTIONS, 300 | TRACE; 301 | } 302 | 303 | class OpenApiLoader { 304 | 305 | fun loadOpenApiSchemes(): Map = 306 | loadVersions() 307 | .ifEmpty { setOf("default") } 308 | .associateWith { loadVersion(it) ?: "{}" } 309 | 310 | fun loadVersions(): Set = 311 | OpenApiLoader::class.java.getResourceAsStream("/openapi-plugin/.index") 312 | ?.readAllBytes() 313 | ?.decodeToString() 314 | ?.split("\n") 315 | ?.asSequence() 316 | ?.map { it.trim() } 317 | ?.map { it.removePrefix("openapi-") } 318 | ?.map { it.removeSuffix(".json") } 319 | ?.toSet() 320 | ?: emptySet() 321 | 322 | fun loadVersion(version: String): String? = 323 | OpenApiLoader::class.java.getResource("/openapi-plugin/openapi-$version.json")?.readText() 324 | 325 | } 326 | -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/Security.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.javalin.openapi 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore 6 | import java.util.function.Consumer 7 | 8 | data class Security @JvmOverloads constructor( 9 | val name: String, 10 | val scopes: MutableList = mutableListOf() 11 | ) { 12 | 13 | fun withScope(scope: String): Security = also { 14 | scopes.add(scope) 15 | } 16 | 17 | } 18 | 19 | interface SecurityScheme { 20 | val type: String 21 | } 22 | 23 | abstract class HttpAuth(val scheme: String) : SecurityScheme { 24 | override val type: String = "http" 25 | } 26 | 27 | class BasicAuth : HttpAuth(scheme = "basic") 28 | 29 | class BearerAuth : HttpAuth(scheme = "bearer") 30 | 31 | open class ApiKeyAuth( 32 | open var `in`: String = "header", 33 | open var name: String = "X-API-Key", 34 | ) : SecurityScheme { 35 | override val type: String = "apiKey" 36 | } 37 | 38 | class CookieAuth @JvmOverloads constructor( 39 | override var name: String, 40 | override var `in`: String = "cookie" 41 | ) : ApiKeyAuth() 42 | 43 | class OpenID (val openIdConnectUrl: String) : SecurityScheme { 44 | override val type: String = "openIdConnect" 45 | } 46 | 47 | class OAuth2 @JvmOverloads constructor( 48 | var description: String, 49 | val flows: MutableMap> = mutableMapOf(), 50 | ) : SecurityScheme { 51 | override val type: String = "oauth2" 52 | 53 | fun withFlow(flow: OAuth2Flow<*>): OAuth2 = also { 54 | flows[flow.flowType] = flow 55 | } 56 | 57 | @JvmOverloads 58 | fun withAuthorizationCodeFlow(authorizationUrl: String, tokenUrl: String, flow: Consumer = Consumer {}): OAuth2 = 59 | withFlow(AuthorizationCodeFlow(authorizationUrl = authorizationUrl, tokenUrl = tokenUrl).also { flow.accept(it) }) 60 | 61 | @JvmOverloads 62 | fun withImplicitFlow(authorizationUrl: String, flow: Consumer = Consumer {}): OAuth2 = 63 | withFlow(ImplicitFlow(authorizationUrl = authorizationUrl).also { flow.accept(it) }) 64 | 65 | @JvmOverloads 66 | fun withPasswordFlow(tokenUrl: String, flow: Consumer = Consumer {}): OAuth2 = 67 | withFlow(PasswordFlow(tokenUrl = tokenUrl).also { flow.accept(it) }) 68 | 69 | @JvmOverloads 70 | fun withClientCredentials(tokenUrl: String, flow: Consumer = Consumer {}): OAuth2 = 71 | withFlow(ClientCredentials(tokenUrl = tokenUrl).also { flow.accept(it) }) 72 | 73 | } 74 | 75 | interface OAuth2Flow> { 76 | @get:JsonIgnore 77 | val flowType: String 78 | val scopes: MutableMap 79 | 80 | @Suppress("UNCHECKED_CAST") 81 | fun withScope(scope: String, description: String): I = also { 82 | scopes[scope] = description 83 | } as I 84 | } 85 | 86 | class AuthorizationCodeFlow @JvmOverloads constructor( 87 | var authorizationUrl: String, 88 | var tokenUrl: String, 89 | override var scopes: MutableMap = mutableMapOf() 90 | ) : OAuth2Flow { 91 | override val flowType: String = "authorizationCode" 92 | } 93 | 94 | class ImplicitFlow @JvmOverloads constructor( 95 | var authorizationUrl: String, 96 | override val scopes: MutableMap = mutableMapOf() 97 | ) : OAuth2Flow { 98 | override val flowType: String = "implicit" 99 | } 100 | 101 | class PasswordFlow @JvmOverloads constructor( 102 | var tokenUrl: String, 103 | override val scopes: MutableMap = mutableMapOf() 104 | ) : OAuth2Flow { 105 | override val flowType: String = "password" 106 | } 107 | 108 | class ClientCredentials @JvmOverloads constructor( 109 | var tokenUrl: String, 110 | override val scopes: MutableMap = mutableMapOf() 111 | ) : OAuth2Flow { 112 | override val flowType: String = "clientCredentials" 113 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/Server.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | /** https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#server-object */ 6 | class OpenApiServer { 7 | 8 | /** REQUIRED. A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in {brackets}. */ 9 | var url: String? = null 10 | fun url(url: String) = apply { this.url = url } 11 | 12 | /** An optional string describing the host designated by the URL. CommonMark syntax MAY be used for rich text representation. */ 13 | var description: String? = null 14 | fun description(description: String) = apply { this.description = description } 15 | 16 | /** A map between a variable name and its value. The value is used for substitution in the server's URL template. */ 17 | var variables: MutableMap = mutableMapOf() 18 | 19 | fun variable(key: String, description: String, defaultValue: String, vararg values: String) = apply { 20 | addVariable(key, OpenApiServerVariable().values(*values).default(defaultValue).description(description)) 21 | } 22 | 23 | fun addVariable(key: String, variable: OpenApiServerVariable): OpenApiServer = also { 24 | variables[key] = variable 25 | } 26 | 27 | fun addVariable(key: String, defaultValue: String, values: Array, description: String): OpenApiServer = also { 28 | addVariable( 29 | key = key, 30 | variable = OpenApiServerVariable().also { 31 | it.values = values.toList() 32 | it.default = defaultValue 33 | it.description = description 34 | } 35 | ) 36 | } 37 | 38 | } 39 | 40 | /** https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#server-variable-object */ 41 | class OpenApiServerVariable { 42 | /** An enumeration of string values to be used if the substitution options are from a limited set. The array MUST NOT be empty. */ 43 | @JsonProperty("enum") 44 | var values: List? = null 45 | fun values(vararg values: String) = apply { this.values = values.toList() } 46 | 47 | /** REQUIRED. The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. Note this behavior is different than the Schema Object's treatment of default values, because in those cases parameter values are optional. If the enum is defined, the value MUST exist in the enum's values. */ 48 | var default: String? = null 49 | fun default(default: String) = apply { this.default = default } 50 | 51 | /** An optional description for the server variable. CommonMark syntax MAY be used for rich text representation. */ 52 | var description: String? = null 53 | fun description(description: String) = apply { this.description = description } 54 | } 55 | -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/data/OpenApiAnnotationsData.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.data 2 | 3 | import io.javalin.openapi.HttpMethod 4 | import io.javalin.openapi.OpenApiCallback 5 | import io.javalin.openapi.OpenApiOperation 6 | import io.javalin.openapi.OpenApiParam 7 | import io.javalin.openapi.OpenApiRequestBody 8 | import io.javalin.openapi.OpenApiResponse 9 | import io.javalin.openapi.OpenApiSecurity 10 | 11 | class OpenApiDocumentation { 12 | 13 | internal class DocumentationState { 14 | var path: String? = null 15 | var methods: List? = null 16 | var versions: List? = listOf("default") 17 | var ignore: Boolean? = false 18 | var summary: String? = null 19 | var description: String? = null 20 | var operationId: String? = null 21 | var deprecated: Boolean? = null 22 | var tags: List? = null 23 | var cookies: List? = null 24 | var headers: List? = null 25 | var pathParams: List? = null 26 | var queryParams: List? = null 27 | var formParams: List? = null 28 | var requestBody: OpenApiRequestBody? = null 29 | var callbacks: List? = null 30 | var responses: List? = null 31 | var security: List? = null 32 | } 33 | 34 | internal val state = DocumentationState() 35 | 36 | /** The described path */ 37 | fun path(path: String) = apply { this.state.path = path } 38 | 39 | /** List of methods to describe **/ 40 | fun methods(vararg methods: HttpMethod) = apply { this.state.methods = methods.toList() } 41 | 42 | /** Schema version **/ 43 | fun versions(vararg versions: String) = apply { this.state.versions = versions.toList() } 44 | 45 | /** Ignore the endpoint in the open api documentation */ 46 | fun ignore(ignore: Boolean) = apply { this.state.ignore = ignore } 47 | 48 | /** An optional, string summary, intended to apply to all operations in this path. **/ 49 | fun summary(summary: String) = apply { this.state.summary = summary } 50 | 51 | /** An optional, string description, intended to apply to all operations in this path. **/ 52 | fun description(description: String) = apply { this.state.description = description } 53 | 54 | /** 55 | * Unique string used to identify the operation. 56 | * The id MUST be unique among all operations described in the API. 57 | * The operationId value is case-sensitive. 58 | * 59 | * You can also use [OpenApiOperation.AUTO_GENERATE] 60 | * if you want to generate the operationId automatically using the method name. 61 | **/ 62 | fun operationId(operationId: String) = apply { this.state.operationId = operationId } 63 | 64 | /** Declares this operation to be deprecated. Consumers SHOULD refrain from usage of the declared operation. **/ 65 | fun deprecated(deprecated: Boolean) = apply { this.state.deprecated = deprecated } 66 | 67 | /** 68 | * A list of tags for API documentation control. 69 | * Tags can be used for logical grouping of operations by resources or any other qualifier. 70 | **/ 71 | fun tags(vararg tags: String) = apply { this.state.tags = tags.toList() } 72 | 73 | /** Describes applicable cookies */ 74 | fun cookies(vararg cookies: OpenApiParam) = apply { this.state.cookies = cookies.toList() } 75 | 76 | /** Describes applicable headers */ 77 | fun headers(vararg headers: OpenApiParam) = apply { this.state.headers = headers.toList() } 78 | 79 | /** Describes applicable path parameters */ 80 | fun pathParams(vararg pathParams: OpenApiParam) = apply { this.state.pathParams = pathParams.toList() } 81 | 82 | /** Describes applicable query parameters */ 83 | fun queryParams(vararg queryParams: OpenApiParam) = apply { this.state.queryParams = queryParams.toList() } 84 | 85 | /** Describes applicable form parameters */ 86 | fun formParams(vararg formParams: OpenApiParam) = apply { this.state.formParams = formParams.toList() } 87 | 88 | /** 89 | * The request body applicable for this operation. 90 | * The requestBody is only supported in HTTP methods where the HTTP 1.1 specification RFC7231 has explicitly defined semantics for request bodies. 91 | * In other cases where the HTTP spec is vague, requestBody SHALL be ignored by consumers. 92 | */ 93 | fun requestBody(requestBody: OpenApiRequestBody) = apply { this.state.requestBody = requestBody } 94 | 95 | /** Describes applicable callbacks */ 96 | fun callbacks(vararg callbacks: OpenApiCallback) = apply { this.state.callbacks = callbacks.toList() } 97 | 98 | /** The list of possible responses as they are returned from executing this operation. */ 99 | fun responses(vararg responses: OpenApiResponse) = apply { this.state.responses = responses.toList() } 100 | 101 | /** A declaration of which security mechanisms can be used for this operation. */ 102 | fun security(vararg security: OpenApiSecurity) = apply { this.state.security = security.toList() } 103 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/AnnotationProcessorContext.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental 2 | 3 | import com.sun.source.util.Trees 4 | import io.javalin.openapi.OpenApiName 5 | import io.javalin.openapi.experimental.StructureType.DEFAULT 6 | import io.javalin.openapi.experimental.processor.generators.TypeSchemaGenerator 7 | import io.javalin.openapi.experimental.processor.shared.getTypeMirror 8 | import io.javalin.openapi.experimental.processor.shared.getTypeMirrors 9 | import javax.annotation.processing.Messager 10 | import javax.annotation.processing.ProcessingEnvironment 11 | import javax.annotation.processing.RoundEnvironment 12 | import javax.lang.model.element.Element 13 | import javax.lang.model.element.ExecutableElement 14 | import javax.lang.model.element.TypeElement 15 | import javax.lang.model.type.TypeMirror 16 | import javax.lang.model.util.Types 17 | import kotlin.reflect.KClass 18 | 19 | class AnnotationProcessorContext( 20 | val parameters: OpenApiAnnotationProcessorParameters, 21 | val configuration: OpenApiAnnotationProcessorConfiguration, 22 | val env: ProcessingEnvironment, 23 | val trees: Trees?, 24 | ) { 25 | 26 | val types: Types = env.typeUtils 27 | val typeSchemaGenerator: TypeSchemaGenerator = TypeSchemaGenerator(this) 28 | var roundEnv: RoundEnvironment? = null 29 | 30 | fun inContext(body: AnnotationProcessorContext.() -> R): R = 31 | body() 32 | 33 | fun inDebug(body: (Messager) -> Unit) { 34 | if (configuration.debug) { 35 | body(env.messager) 36 | } 37 | } 38 | 39 | fun getClassDefinition(mirror: TypeMirror, generics: List = emptyList(), type: StructureType = DEFAULT): ClassDefinition = 40 | ClassDefinition.classDefinitionFrom(this, mirror, generics, type) 41 | 42 | fun getClassDefinitions(mirrors: Set): Set = 43 | mirrors.map { getClassDefinition(it) }.toSet() 44 | 45 | fun forTypeElement(name: String): TypeElement? = 46 | env.elementUtils.getTypeElement(name) 47 | 48 | fun forTypeElement(mirror: TypeMirror): TypeElement = 49 | env.typeUtils.asElement(mirror) as TypeElement 50 | 51 | fun isAssignable(implementation: TypeMirror, superclass: TypeMirror): Boolean = 52 | env.typeUtils.isAssignable(implementation, superclass) 53 | 54 | fun hasElement(type: TypeElement, element: Element): Boolean = 55 | when (element) { 56 | is ExecutableElement -> env.elementUtils.getAllMembers(type).let { members -> 57 | members.contains(element) || members.filterIsInstance().any { env.elementUtils.overrides(element, it, type) } 58 | } 59 | else -> false 60 | } 61 | 62 | fun getFullName(mirror: TypeMirror): String = 63 | env.typeUtils.asElement(mirror) 64 | ?.getAnnotation(OpenApiName::class.java) 65 | ?.value 66 | ?.let { mirror.toString().substringBeforeLast(".") + "." + it } 67 | ?: env.typeUtils.asElement(mirror)?.toString()?.substringBefore("<") 68 | ?: mirror.toString().substringBefore("<") 69 | 70 | /* Extension methods, should be replaced by context receivers in the future */ 71 | 72 | fun TypeMirror.toClassDefinition( 73 | generics: List = emptyList(), 74 | type: StructureType = DEFAULT 75 | ): ClassDefinition = getClassDefinition(this, generics, type) 76 | 77 | fun TypeMirror.getSimpleName(): String = 78 | getFullName().substringAfterLast(".") 79 | 80 | @JvmName("getFullNameExt") 81 | fun TypeMirror.getFullName(): String = 82 | getFullName(this) 83 | 84 | fun A.getClassDefinitions(supplier: A.() -> Array>): Set = 85 | getTypeMirrors(supplier) 86 | .map { it.toClassDefinition() } 87 | .toSet() 88 | 89 | fun A.getClassDefinition(supplier: A.() -> KClass<*>): ClassDefinition = 90 | getTypeMirror(supplier).toClassDefinition() 91 | 92 | } 93 | 94 | -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/ClassDefinitionApi.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental 2 | 3 | import io.javalin.openapi.experimental.StructureType.ARRAY 4 | import io.javalin.openapi.experimental.StructureType.DEFAULT 5 | import io.javalin.openapi.experimental.StructureType.DICTIONARY 6 | import io.javalin.openapi.experimental.processor.shared.collectionType 7 | import io.javalin.openapi.experimental.processor.shared.mapType 8 | import io.javalin.openapi.experimental.processor.shared.objectType 9 | import javax.lang.model.element.Element 10 | import javax.lang.model.type.ArrayType 11 | import javax.lang.model.type.DeclaredType 12 | import javax.lang.model.type.PrimitiveType 13 | import javax.lang.model.type.TypeMirror 14 | import javax.lang.model.type.TypeVariable 15 | 16 | class ClassDefinition( 17 | val context: AnnotationProcessorContext, 18 | val mirror: TypeMirror, 19 | val source: Element, 20 | val generics: List = emptyList(), 21 | val structureType: StructureType = DEFAULT, 22 | val extra: MutableList = mutableListOf() 23 | ) { 24 | 25 | val simpleName: String = context.inContext { mirror.getSimpleName() } 26 | val fullName: String = context.inContext { mirror.getFullName() } 27 | 28 | companion object { 29 | 30 | @JvmStatic 31 | fun classDefinitionFrom( 32 | context: AnnotationProcessorContext, 33 | mirror: TypeMirror, 34 | generics: List = emptyList(), 35 | type: StructureType = DEFAULT 36 | ): ClassDefinition = 37 | with(context) { 38 | with (mirror) { 39 | when (this) { 40 | is TypeVariable -> 41 | upperBound?.toClassDefinition(generics, type) ?: lowerBound?.toClassDefinition(generics, type) 42 | is ArrayType -> 43 | componentType.toClassDefinition(generics, type = ARRAY) 44 | is PrimitiveType -> 45 | ClassDefinition( 46 | context = context, 47 | mirror = types.boxedClass(this).asType(), 48 | source = types.boxedClass(this), 49 | generics = generics, 50 | structureType = type 51 | ) 52 | is DeclaredType -> 53 | when { 54 | types.isAssignable(types.erasure(this), mapType().asType()) -> 55 | ClassDefinition( 56 | context = context, 57 | mirror = this, 58 | source = mapType(), 59 | generics = listOfNotNull( 60 | typeArguments.getOrElse(0) { objectType().asType() }.toClassDefinition(), 61 | typeArguments.getOrElse(1) { objectType().asType() }.toClassDefinition() 62 | ), 63 | structureType = DICTIONARY 64 | ) 65 | types.isAssignable(types.erasure(this), collectionType().asType()) -> 66 | typeArguments.getOrElse(0) { objectType().asType() }.toClassDefinition(generics, ARRAY) 67 | else -> 68 | ClassDefinition( 69 | context = context, 70 | mirror = this, 71 | source = asElement(), 72 | generics = typeArguments.mapNotNull { it.toClassDefinition() }, 73 | structureType = type 74 | ) 75 | } 76 | else -> 77 | types.asElement(this)?.asType()?.toClassDefinition(generics, type) 78 | } 79 | } ?: objectType().asType().toClassDefinition(type = type) 80 | } 81 | 82 | } 83 | 84 | override fun equals(other: Any?): Boolean = 85 | when { 86 | this === other -> true 87 | other is ClassDefinition -> this.fullName == other.fullName 88 | else -> false 89 | } 90 | 91 | override fun hashCode(): Int = fullName.hashCode() 92 | 93 | } 94 | 95 | enum class StructureType { 96 | DEFAULT, 97 | ARRAY, 98 | DICTIONARY 99 | } 100 | 101 | interface Extra 102 | 103 | class CustomProperty( 104 | val name: String, 105 | val type: ClassDefinition 106 | ) : Extra 107 | -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/OpenApiAnnotationProcessorConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental 2 | 3 | import com.google.gson.JsonObject 4 | import io.javalin.openapi.experimental.defaults.ArrayEmbeddedTypeProcessor 5 | import io.javalin.openapi.experimental.defaults.CompositionEmbeddedTypeProcessor 6 | import io.javalin.openapi.experimental.defaults.DictionaryEmbeddedTypeProcessor 7 | import io.javalin.openapi.experimental.defaults.createDefaultSimpleTypeMappings 8 | import io.javalin.openapi.experimental.processor.generators.PropertyComposition 9 | import javax.lang.model.element.Element 10 | import kotlin.annotation.AnnotationRetention.BINARY 11 | import kotlin.annotation.AnnotationTarget.CLASS 12 | import kotlin.annotation.AnnotationTarget.FUNCTION 13 | 14 | @RequiresOptIn 15 | @Retention(BINARY) 16 | @Target(CLASS, FUNCTION) 17 | annotation class ExperimentalCompileOpenApiConfiguration 18 | 19 | @ExperimentalCompileOpenApiConfiguration 20 | interface OpenApiAnnotationProcessorConfigurer { 21 | fun configure(configuration: OpenApiAnnotationProcessorConfiguration) 22 | } 23 | 24 | data class SimpleType @JvmOverloads constructor( 25 | val type: String, 26 | val format: String? = null 27 | ) 28 | 29 | class OpenApiAnnotationProcessorConfiguration { 30 | var debug: Boolean = false 31 | var validateWithParser: Boolean = true 32 | var propertyInSchemeFilter: PropertyInSchemeFilter? = null 33 | val simpleTypeMappings: MutableMap = createDefaultSimpleTypeMappings() 34 | val embeddedTypeProcessors: MutableList = mutableListOf( 35 | CompositionEmbeddedTypeProcessor(), 36 | ArrayEmbeddedTypeProcessor(), 37 | DictionaryEmbeddedTypeProcessor() 38 | ) 39 | 40 | fun insertEmbeddedTypeProcessor(embeddedTypeProcessor: EmbeddedTypeProcessor) { 41 | embeddedTypeProcessors.add(0, embeddedTypeProcessor) 42 | } 43 | 44 | } 45 | 46 | fun interface PropertyInSchemeFilter { 47 | fun filter(context: AnnotationProcessorContext, type: ClassDefinition, property: Element): Boolean 48 | } 49 | 50 | data class EmbeddedTypeProcessorContext( 51 | val parentContext: AnnotationProcessorContext, 52 | val scheme: JsonObject, 53 | val references: MutableSet, 54 | val type: ClassDefinition, 55 | val inlineRefs: Boolean = false, 56 | val requiresNonNulls: Boolean = true, 57 | val composition: PropertyComposition? = null, 58 | val extra: Map = emptyMap() 59 | ) 60 | 61 | fun interface EmbeddedTypeProcessor { 62 | fun process(context: EmbeddedTypeProcessorContext): Boolean 63 | } 64 | -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/OpenApiAnnotationProcessorParameters.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental 2 | 3 | const val OPENAPI_INFO_TITLE = "openapi.info.title" 4 | const val OPENAPI_INFO_VERSION = "openapi.info.version" 5 | 6 | data class OpenApiAnnotationProcessorParameters( 7 | val info: Info 8 | ) { 9 | 10 | data class Info( 11 | val title: String, 12 | val version: String, 13 | ) 14 | 15 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/defaults/ArrayEmbeddedTypeProcessor.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental.defaults 2 | 3 | import com.google.gson.JsonObject 4 | import io.javalin.openapi.experimental.EmbeddedTypeProcessor 5 | import io.javalin.openapi.experimental.EmbeddedTypeProcessorContext 6 | import io.javalin.openapi.experimental.StructureType.ARRAY 7 | 8 | class ArrayEmbeddedTypeProcessor : EmbeddedTypeProcessor { 9 | 10 | override fun process(context: EmbeddedTypeProcessorContext): Boolean = with(context) { 11 | if (type.structureType == ARRAY) { 12 | if (type.simpleName == "Byte") { 13 | scheme.addProperty("type", "string") 14 | scheme.addProperty("format", "binary") 15 | } 16 | else { 17 | context.scheme.addProperty("type", "array") 18 | val items = JsonObject() 19 | context.parentContext.typeSchemaGenerator.addType(items, type, inlineRefs, references, requiresNonNulls) 20 | context.scheme.add("items", items) 21 | } 22 | 23 | return true 24 | } 25 | 26 | return false 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/defaults/CompositionEmbeddedTypeProcessor.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental.defaults 2 | 3 | import io.javalin.openapi.experimental.EmbeddedTypeProcessor 4 | import io.javalin.openapi.experimental.EmbeddedTypeProcessorContext 5 | import io.javalin.openapi.experimental.processor.generators.createComposition 6 | 7 | class CompositionEmbeddedTypeProcessor : EmbeddedTypeProcessor { 8 | 9 | override fun process(context: EmbeddedTypeProcessorContext): Boolean = 10 | context.composition 11 | ?.let { 12 | context.scheme.createComposition( 13 | context = context.parentContext, 14 | classDefinition = context.type, 15 | propertyComposition = it, 16 | references = context.references, 17 | inlineRefs = context.inlineRefs, 18 | requiresNonNulls = context.requiresNonNulls 19 | ) 20 | true 21 | } ?: false 22 | 23 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/defaults/DefaultSimpleTypeMappings.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental.defaults 2 | 3 | import io.javalin.openapi.experimental.SimpleType 4 | 5 | internal fun createDefaultSimpleTypeMappings(): MutableMap = mutableMapOf( 6 | "boolean" to SimpleType("boolean"), 7 | "java.lang.Boolean" to SimpleType("boolean"), 8 | 9 | "byte" to SimpleType("integer", "int32"), 10 | "java.lang.Byte" to SimpleType("integer", "int32"), 11 | "short" to SimpleType("integer", "int32"), 12 | "java.lang.Short" to SimpleType("integer", "int32"), 13 | "int" to SimpleType("integer", "int32"), 14 | "java.lang.Integer" to SimpleType("integer", "int32"), 15 | "long" to SimpleType("integer", "int64"), 16 | "java.lang.Long" to SimpleType("integer", "int64"), 17 | 18 | "float" to SimpleType("number", "float"), 19 | "java.lang.Float" to SimpleType("number", "float"), 20 | "double" to SimpleType("number", "double"), 21 | "java.lang.Double" to SimpleType("number", "double"), 22 | 23 | "char" to SimpleType("string"), 24 | "java.lang.Character" to SimpleType("string"), 25 | "java.lang.String" to SimpleType("string"), 26 | "java.math.BigDecimal" to SimpleType("string"), 27 | "java.util.UUID" to SimpleType("string"), 28 | "org.bson.types.ObjectId" to SimpleType("string"), 29 | 30 | "byte[]" to SimpleType("string", "binary"), 31 | "java.io.InputStream" to SimpleType("string", "binary"), 32 | "java.io.File" to SimpleType("string", "binary"), 33 | 34 | "java.util.Date" to SimpleType("string", "date"), 35 | "java.time.LocalDate" to SimpleType("string", "date"), 36 | 37 | "java.time.LocalDateTime" to SimpleType("string", "date-time"), 38 | "java.time.ZonedDateTime" to SimpleType("string", "date-time"), 39 | "java.time.Instant" to SimpleType("string", "date-time"), 40 | 41 | "java.lang.Object" to SimpleType("object"), 42 | "java.util.Map" to SimpleType("object"), 43 | ) -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/defaults/DictionaryEmbeddedTypeProcessor.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental.defaults 2 | 3 | import com.google.gson.JsonObject 4 | import io.javalin.openapi.experimental.EmbeddedTypeProcessor 5 | import io.javalin.openapi.experimental.EmbeddedTypeProcessorContext 6 | import io.javalin.openapi.experimental.StructureType.DICTIONARY 7 | 8 | class DictionaryEmbeddedTypeProcessor : EmbeddedTypeProcessor { 9 | 10 | override fun process(context: EmbeddedTypeProcessorContext): Boolean = with (context) { 11 | if (type.structureType == DICTIONARY) { 12 | scheme.addProperty("type", "object") 13 | val additionalProperties = JsonObject() 14 | val additionalType = context.type.generics[1] 15 | 16 | context.parentContext.configuration.embeddedTypeProcessors 17 | .firstOrNull { 18 | it.process( 19 | context.copy( 20 | scheme = additionalProperties, 21 | type = additionalType 22 | ) 23 | ) 24 | } 25 | ?: parentContext.typeSchemaGenerator.addType( 26 | scheme = additionalProperties, 27 | type = additionalType, 28 | inlineRefs = inlineRefs, 29 | references = references, 30 | requiresNonNulls = requiresNonNulls 31 | ) 32 | 33 | scheme.add("additionalProperties", additionalProperties) 34 | return true 35 | } 36 | 37 | return false 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/generators/CompositionGenerator.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental.processor.generators 2 | 3 | import com.google.gson.JsonObject 4 | import io.javalin.openapi.AllOf 5 | import io.javalin.openapi.AnyOf 6 | import io.javalin.openapi.Composition 7 | import io.javalin.openapi.Composition.ALL_OF 8 | import io.javalin.openapi.Composition.ANY_OF 9 | import io.javalin.openapi.Composition.ONE_OF 10 | import io.javalin.openapi.Discriminator 11 | import io.javalin.openapi.DiscriminatorMappingName 12 | import io.javalin.openapi.NULL_STRING 13 | import io.javalin.openapi.OneOf 14 | import io.javalin.openapi.experimental.AnnotationProcessorContext 15 | import io.javalin.openapi.experimental.ClassDefinition 16 | import io.javalin.openapi.experimental.CustomProperty 17 | import io.javalin.openapi.experimental.processor.shared.createJsonObjectOf 18 | import io.javalin.openapi.experimental.processor.shared.toJsonArray 19 | import io.javalin.openapi.experimental.processor.shared.toJsonObject 20 | import javax.lang.model.element.Element 21 | import javax.lang.model.element.TypeElement 22 | 23 | data class PropertyComposition( 24 | val type: Composition, 25 | val references: Set, 26 | val discriminator: Discriminator 27 | ) 28 | 29 | fun findCompositionInElement(context: AnnotationProcessorContext, element: Element): PropertyComposition? = 30 | with (context) { 31 | element.getAnnotation(OneOf::class.java)?.let { PropertyComposition(ONE_OF, it.getClassDefinitions { value }, it.discriminator) } 32 | ?: element.getAnnotation(AnyOf::class.java)?.let { PropertyComposition(ANY_OF, it.getClassDefinitions { value }, it.discriminator) } 33 | ?: element.getAnnotation(AllOf::class.java)?.let { PropertyComposition(ALL_OF, it.getClassDefinitions { value }, it.discriminator) } 34 | } 35 | 36 | fun JsonObject.createComposition( 37 | context: AnnotationProcessorContext, 38 | classDefinition: ClassDefinition, 39 | propertyComposition: PropertyComposition, 40 | references: MutableSet, 41 | inlineRefs: Boolean = false, 42 | requiresNonNulls: Boolean = true, 43 | ) { 44 | with (context) { 45 | val subtypes by lazy { 46 | context.roundEnv!!.getElementsAnnotatedWith(DiscriminatorMappingName::class.java) 47 | .asSequence() 48 | .filterIsInstance() 49 | .map { it.getAnnotation(DiscriminatorMappingName::class.java).value to context.getClassDefinition(it.asType()) } 50 | .filter { (_, type) -> context.isAssignable(type.mirror, classDefinition.mirror) } 51 | .toList() 52 | } 53 | 54 | val refs = propertyComposition.references.ifEmpty { subtypes.map { it.second } } 55 | 56 | when (inlineRefs) { 57 | true -> 58 | refs 59 | .map { context.typeSchemaGenerator.createTypeSchema(type = it, inlineRefs = true, requireNonNullsByDefault = requiresNonNulls) } 60 | .onEach { (_, refs) -> references.addAll(refs) } 61 | .map { (scheme, _) -> scheme } 62 | .toJsonArray { add(it) } 63 | .let { add(propertyComposition.type.propertyName, it) } 64 | 65 | false -> 66 | refs 67 | .onEach { references.add(it) } 68 | .map { createJsonObjectOf("\$ref", "#/components/schemas/${it.simpleName}") } 69 | .toJsonArray { add(it) } 70 | .let { add(propertyComposition.type.propertyName, it) } 71 | } 72 | 73 | propertyComposition.discriminator 74 | .takeIf { it.property.name != NULL_STRING } 75 | ?.also { discriminator -> 76 | val discriminatorObject = JsonObject() 77 | add("discriminator", discriminatorObject) 78 | 79 | val discriminatorProperty = discriminator.property 80 | discriminatorObject.addProperty("propertyName", discriminatorProperty.name) 81 | 82 | val mapping = discriminator.mapping 83 | .map { it.name to it.getClassDefinition { value } } 84 | .ifEmpty { subtypes } 85 | 86 | if (discriminatorProperty.injectInMappings) { 87 | val customProperty = CustomProperty( 88 | name = discriminatorProperty.name, 89 | type = discriminatorProperty.getClassDefinition { type } 90 | ) 91 | 92 | mapping.forEach { (_, mappedClass) -> 93 | mappedClass.extra.add(customProperty) 94 | } 95 | } 96 | 97 | mapping 98 | .onEach { (_, mappedClass) -> references.add(mappedClass) } 99 | .associate { (name, mappedClass) -> name to "#/components/schemas/${mappedClass.simpleName}" } 100 | .takeIf { it.isNotEmpty() } 101 | ?.also { discriminatorObject.add("mapping", it.toJsonObject()) } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/generators/ExampleGenerator.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental.processor.generators 2 | 3 | import com.google.gson.JsonArray 4 | import com.google.gson.JsonElement 5 | import com.google.gson.JsonObject 6 | import io.javalin.openapi.NULL_STRING 7 | import io.javalin.openapi.OpenApiExampleProperty 8 | 9 | object ExampleGenerator { 10 | 11 | data class ExampleProperty( 12 | val name: String?, 13 | val value: String?, 14 | val objects: List? 15 | ) 16 | 17 | fun OpenApiExampleProperty.toExampleProperty(): ExampleProperty = 18 | ExampleProperty(this.name, this.value, this.objects.map { it.toExampleProperty() }) 19 | 20 | data class GeneratorResult(val simpleValue: String?, val jsonElement: JsonElement?) { 21 | init { 22 | when { 23 | simpleValue != null && jsonElement != null -> throw IllegalArgumentException("rawList and jsonElement cannot be both non-null") 24 | simpleValue == null && jsonElement == null -> throw IllegalArgumentException("rawList and jsonElement cannot be both null") 25 | } 26 | } 27 | } 28 | 29 | fun generateFromExamples(examples: List): GeneratorResult { 30 | if (examples.isRawList()) { 31 | val jsonArray = JsonArray() 32 | examples.forEach { jsonArray.add(it.value) } 33 | return GeneratorResult(null, jsonArray) 34 | } 35 | 36 | if (examples.isObjectList()) { 37 | val jsonArray = JsonArray() 38 | examples.forEach { jsonArray.add(it.toSimpleExampleValue().jsonElement!!) } 39 | return GeneratorResult(null, jsonArray) 40 | } 41 | 42 | return GeneratorResult(null, examples.toJsonObject()) 43 | } 44 | 45 | private fun ExampleProperty.toSimpleExampleValue(): GeneratorResult = 46 | when { 47 | this.value != NULL_STRING -> GeneratorResult(this.value, null) 48 | this.objects?.isNotEmpty() == true-> GeneratorResult(null, objects.toJsonObject()) 49 | else -> throw IllegalArgumentException("Example object must have either value or objects ($this)") 50 | } 51 | 52 | private fun List.toJsonObject(): JsonObject { 53 | val jsonObject = JsonObject() 54 | this.forEach { 55 | val result = it.toSimpleExampleValue() 56 | if (it.name == NULL_STRING) { 57 | throw IllegalArgumentException("Example object must have a name ($it)") 58 | } 59 | when { 60 | result.simpleValue != null -> jsonObject.addProperty(it.name, result.simpleValue) 61 | result.jsonElement != null -> jsonObject.add(it.name, result.jsonElement) 62 | } 63 | } 64 | return jsonObject 65 | } 66 | 67 | private fun List.isObjectList(): Boolean = 68 | this.isNotEmpty() && this.all { it.name == NULL_STRING && it.value == NULL_STRING && it.objects?.isNotEmpty() ?: false } 69 | 70 | private fun List.isRawList(): Boolean = 71 | this.isNotEmpty() && this.all { it.name == NULL_STRING && it.value != NULL_STRING && it.objects?.isEmpty() ?: true } 72 | 73 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/shared/AnnotationProcessorExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental.processor.shared 2 | 3 | import io.javalin.openapi.experimental.AnnotationProcessorContext 4 | import java.io.Writer 5 | import javax.annotation.processing.Filer 6 | import javax.annotation.processing.FilerException 7 | import javax.annotation.processing.Messager 8 | import javax.tools.Diagnostic.Kind 9 | import javax.tools.Diagnostic.Kind.ERROR 10 | import javax.tools.Diagnostic.Kind.NOTE 11 | import javax.tools.FileObject 12 | import javax.tools.StandardLocation 13 | 14 | fun Filer.saveResource(context: AnnotationProcessorContext, name: String, content: String): FileObject? = 15 | try { 16 | val resource = createResource(StandardLocation.CLASS_OUTPUT, "", name) 17 | resource.openWriter().use { 18 | it.write(content) 19 | } 20 | resource 21 | } catch (filerException: FilerException) { 22 | // file has been created during previous compilation phase 23 | null 24 | } catch (throwable: Throwable) { 25 | context.env.messager.printException(throwable) 26 | null 27 | } 28 | 29 | fun Messager.info(message: String) = 30 | printMessage(NOTE, message) 31 | 32 | fun Messager.printException(throwable: Throwable) { 33 | printException(ERROR, throwable) 34 | } 35 | 36 | fun Messager.printException(kind: Kind, throwable: Throwable) { 37 | val error = StringBuilder(throwable.javaClass.toString() + ": " + throwable.message) 38 | 39 | for (element in throwable.stackTrace) { 40 | error.append(" ").append(element.toString()).append(System.lineSeparator()) 41 | } 42 | 43 | printMessage(kind, error.toString()) 44 | 45 | if (throwable.cause != null) { 46 | printMessage(kind, "---") 47 | printException(throwable.cause!!) 48 | } 49 | } 50 | 51 | class MessagerWriter(val context: AnnotationProcessorContext) : Writer() { 52 | 53 | private val builder = StringBuilder() 54 | 55 | override fun flush() { 56 | context.env.messager.info(builder.toString()) 57 | builder.clear() 58 | } 59 | 60 | override fun write(cbuf: CharArray, off: Int, len: Int) { 61 | builder.append(cbuf, off, len) 62 | } 63 | 64 | override fun close() { 65 | flush() 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/shared/JsonExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental.processor.shared 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.GsonBuilder 5 | import com.google.gson.JsonArray 6 | import com.google.gson.JsonObject 7 | import io.javalin.openapi.NULL_STRING 8 | 9 | private val gson: Gson = GsonBuilder() 10 | .setPrettyPrinting() 11 | .create() 12 | 13 | fun JsonObject.toPrettyString(): String = 14 | gson.toJson(this) 15 | 16 | fun Map.toJsonObject(): JsonObject { 17 | val jsonObject = JsonObject() 18 | forEach { (key, value) -> jsonObject.addProperty(key, value) } 19 | return jsonObject 20 | } 21 | 22 | fun List.toJsonArray(accumulator: JsonArray.(T) -> Unit): JsonArray { 23 | val jsonArray = JsonArray(size) 24 | forEach { accumulator(jsonArray, it) } 25 | return jsonArray 26 | } 27 | 28 | fun Array.toJsonArray(mapper: (T) -> String = { it.toString() }): JsonArray { 29 | val jsonArray = JsonArray(size) 30 | map(mapper).forEach { jsonArray.add(it) } 31 | return jsonArray 32 | } 33 | 34 | fun JsonObject.computeIfAbsent(key: String, value: () -> JsonObject): JsonObject { 35 | if (!has(key)) { 36 | add(key, value()) 37 | } 38 | 39 | return getAsJsonObject(key) 40 | } 41 | 42 | fun JsonObject.addString(key: String, value: String?): JsonObject = also { 43 | if (NULL_STRING != value) { 44 | addProperty(key, value) 45 | } 46 | } 47 | 48 | fun JsonObject.addIfNotEmpty(key: String, value: JsonObject): JsonObject = also { 49 | if (value.size() > 0) { 50 | add(key, value) 51 | } 52 | } 53 | 54 | fun createJsonObjectOf(key: String, value: String): JsonObject { 55 | val jsonObject = JsonObject() 56 | jsonObject.addProperty(key, value) 57 | return jsonObject 58 | } -------------------------------------------------------------------------------- /openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/shared/ModelExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.javalin.openapi.experimental.processor.shared 2 | 3 | import io.javalin.openapi.experimental.AnnotationProcessorContext 4 | import javax.lang.model.element.Element 5 | import javax.lang.model.element.TypeElement 6 | import javax.lang.model.element.VariableElement 7 | import javax.lang.model.type.MirroredTypeException 8 | import javax.lang.model.type.MirroredTypesException 9 | import javax.lang.model.type.TypeMirror 10 | import kotlin.reflect.KClass 11 | 12 | fun AnnotationProcessorContext.objectType(): TypeElement = forTypeElement(Object::class.java.name)!! 13 | fun AnnotationProcessorContext.collectionType(): TypeElement = forTypeElement(Collection::class.java.name)!! 14 | fun AnnotationProcessorContext.mapType(): TypeElement = forTypeElement(Map::class.java.name)!! 15 | fun AnnotationProcessorContext.recordType(): TypeElement? = forTypeElement("java.lang.Record") 16 | 17 | fun TypeMirror.isPrimitive(): Boolean = 18 | kind.isPrimitive 19 | 20 | fun Element.hasAnnotation(simpleName: String): Boolean = 21 | annotationMirrors.any { it.annotationType.asElement().simpleName.contentEquals(simpleName) } 22 | 23 | fun Element.getFullName(): String = 24 | toString() 25 | 26 | fun Element.toSimpleName(): String = 27 | simpleName.toString() 28 | 29 | fun VariableElement.toSimpleName(): String = 30 | simpleName.toString() 31 | 32 | fun A.getTypeMirrors(supplier: A.() -> Array>): Set = 33 | try { 34 | throw Error(supplier().toString()) // always throws MirroredTypesException, because we cannot get Class instance from annotation at compile-time 35 | } catch (mirroredTypeException: MirroredTypesException) { 36 | mirroredTypeException.typeMirrors.toSet() 37 | } 38 | 39 | fun A.getTypeMirror(supplier: A.() -> KClass<*>): TypeMirror = 40 | try { 41 | throw Error(supplier().toString()) // always throws MirroredTypeException, because we cannot get Class instance from annotation at compile-time 42 | } catch (mirroredTypeException: MirroredTypeException) { 43 | mirroredTypeException.typeMirror 44 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "javalin-openapi" 2 | 3 | include( 4 | "openapi-specification", 5 | "openapi-annotation-processor", 6 | "javalin-plugins", 7 | "javalin-plugins:javalin-openapi-plugin", 8 | "javalin-plugins:javalin-swagger-plugin", 9 | "javalin-plugins:javalin-redoc-plugin", 10 | "examples", 11 | "examples:javalin-gradle-kotlin" 12 | ) --------------------------------------------------------------------------------