├── .editorconfig ├── .github └── workflows │ ├── checks.yml │ ├── publish.yml │ └── publish_docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── detekt └── detekt.yml ├── docs ├── assets │ ├── icons │ │ └── redoc.svg │ ├── images │ │ ├── redoc.png │ │ └── swagger.png │ └── stylesheets │ │ └── extra.css ├── changelog.md ├── examples │ ├── authentication.md │ ├── basics.md │ ├── complete-configuration.md │ ├── customized-schema-generator.md │ ├── external-openapi-spec.md │ ├── file-upload.md │ ├── index.md │ ├── kotlinx-serialization.md │ ├── multiple-specs.md │ ├── openapi-examples.md │ ├── petstore.md │ ├── request-response.md │ ├── schemas.md │ └── webhooks.md ├── index.md ├── openapi │ ├── documenting_routes.md │ ├── example_encoding.md │ ├── getting_started.md │ ├── handling_types_schemas_examples.md │ ├── index.md │ ├── multiple_specs.md │ ├── plugin_configuration.md │ ├── schema_generation.md │ └── typesafe_routing.md ├── redoc │ ├── getting_started.md │ └── index.md └── swaggerui │ ├── getting_started.md │ └── index.md ├── examples ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── io │ │ └── github │ │ └── smiley4 │ │ └── ktoropenapi │ │ └── examples │ │ ├── Authentication.kt │ │ ├── Basics.kt │ │ ├── CompleteConfig.kt │ │ ├── CustomizedSchemaGenerator.kt │ │ ├── Examples.kt │ │ ├── ExternalOpenApiSpec.kt │ │ ├── FileUpload.kt │ │ ├── KotlinxSerialization.kt │ │ ├── Minimal.kt │ │ ├── MultipleSpecs.kt │ │ ├── Petstore.kt │ │ ├── RequestResponse.kt │ │ ├── Schemas.kt │ │ ├── TypesafeRouting.kt │ │ └── Webhooks.kt │ └── resources │ └── logback.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ktor-openapi ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── smiley4 │ │ └── ktoropenapi │ │ ├── DocumentedRouteSelector.kt │ │ ├── OpenApiPlugin.kt │ │ ├── builder │ │ ├── OpenApiSpecBuilder.kt │ │ ├── example │ │ │ ├── ExampleContext.kt │ │ │ └── ExampleContextImpl.kt │ │ ├── openapi │ │ │ ├── ComponentsBuilder.kt │ │ │ ├── ContactBuilder.kt │ │ │ ├── ContentBuilder.kt │ │ │ ├── ExternalDocumentationBuilder.kt │ │ │ ├── HeaderBuilder.kt │ │ │ ├── InfoBuilder.kt │ │ │ ├── LicenseBuilder.kt │ │ │ ├── OAuthFlowsBuilder.kt │ │ │ ├── OpenApiBuilder.kt │ │ │ ├── OperationBuilder.kt │ │ │ ├── OperationTagsBuilder.kt │ │ │ ├── ParameterBuilder.kt │ │ │ ├── PathBuilder.kt │ │ │ ├── PathsBuilder.kt │ │ │ ├── RequestBodyBuilder.kt │ │ │ ├── ResponseBuilder.kt │ │ │ ├── ResponsesBuilder.kt │ │ │ ├── SecurityRequirementsBuilder.kt │ │ │ ├── SecuritySchemesBuilder.kt │ │ │ ├── ServerBuilder.kt │ │ │ ├── TagBuilder.kt │ │ │ ├── TagExternalDocumentationBuilder.kt │ │ │ └── WebhooksBuilder.kt │ │ ├── route │ │ │ ├── RouteCollector.kt │ │ │ ├── RouteDocumentationMerger.kt │ │ │ └── RouteMeta.kt │ │ └── schema │ │ │ ├── SchemaContext.kt │ │ │ └── SchemaContextImpl.kt │ │ ├── config │ │ ├── AuthKeyLocation.kt │ │ ├── AuthScheme.kt │ │ ├── AuthType.kt │ │ ├── BaseBodyConfig.kt │ │ ├── ContactConfig.kt │ │ ├── ExampleConfig.kt │ │ ├── ExampleEncoder.kt │ │ ├── ExternalDocsConfig.kt │ │ ├── HeaderConfig.kt │ │ ├── InfoConfig.kt │ │ ├── LicenseConfig.kt │ │ ├── MultipartBodyConfig.kt │ │ ├── MultipartPartConfig.kt │ │ ├── OpenApiDslMarker.kt │ │ ├── OpenApiPluginConfig.kt │ │ ├── OpenIdOAuthFlowConfig.kt │ │ ├── OpenIdOAuthFlowsConfig.kt │ │ ├── OutputFormat.kt │ │ ├── ParameterLocation.kt │ │ ├── PathFilter.kt │ │ ├── PostBuild.kt │ │ ├── RequestConfig.kt │ │ ├── RequestParameterConfig.kt │ │ ├── ResponseConfig.kt │ │ ├── ResponsesConfig.kt │ │ ├── RouteConfig.kt │ │ ├── SchemaConfig.kt │ │ ├── SchemaGenerator.kt │ │ ├── SchemaOverwriteModule.kt │ │ ├── SecurityConfig.kt │ │ ├── SecuritySchemeConfig.kt │ │ ├── ServerConfig.kt │ │ ├── ServerVariableConfig.kt │ │ ├── SimpleBodyConfig.kt │ │ ├── SpecAssigner.kt │ │ ├── TagConfig.kt │ │ ├── TagGenerator.kt │ │ ├── TagsConfig.kt │ │ ├── ValueExampleDescriptorConfig.kt │ │ └── descriptors │ │ │ ├── ExampleDescriptor.kt │ │ │ └── TypeDescriptor.kt │ │ ├── data │ │ ├── BaseBodyData.kt │ │ ├── ContactData.kt │ │ ├── DataUtils.kt │ │ ├── ExampleConfigData.kt │ │ ├── ExternalDocsData.kt │ │ ├── HeaderData.kt │ │ ├── InfoData.kt │ │ ├── LicenseData.kt │ │ ├── MultipartPartData.kt │ │ ├── OpenApiPluginData.kt │ │ ├── OpenIdOAuthFlowData.kt │ │ ├── OpenIdOAuthFlowsData.kt │ │ ├── RequestData.kt │ │ ├── RequestParameterData.kt │ │ ├── ResponseData.kt │ │ ├── RouteData.kt │ │ ├── SchemaConfigData.kt │ │ ├── SecurityData.kt │ │ ├── SecuritySchemeData.kt │ │ ├── ServerData.kt │ │ ├── ServerVariableData.kt │ │ ├── TagData.kt │ │ └── TagsData.kt │ │ └── resources │ │ ├── DocumentedResourceRoutes.kt │ │ └── documentationExtractor.kt │ └── test │ ├── kotlin │ └── io │ │ └── github │ │ └── smiley4 │ │ └── ktorswaggerui │ │ ├── builder │ │ ├── ExternalDocsBuilderTest.kt │ │ ├── InfoBuilderTest.kt │ │ ├── OpenApiBuilderTest.kt │ │ ├── OperationBuilderTest.kt │ │ ├── PathsBuilderTest.kt │ │ ├── SecuritySchemesBuilderTest.kt │ │ ├── ServersBuilderTest.kt │ │ └── TagsBuilderTest.kt │ │ └── misc │ │ ├── RouteDocumentationMergerTest.kt │ │ └── RoutingTests.kt │ └── resources │ └── logback.xml ├── ktor-redoc ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── smiley4 │ │ └── ktorredoc │ │ ├── PropertyBuilder.kt │ │ ├── Redoc.kt │ │ ├── RedocRouteSelector.kt │ │ ├── ResourceContent.kt │ │ └── config │ │ └── RedocConfig.kt │ └── test │ └── kotlin │ └── io │ └── github │ └── smiley4 │ └── ktorredoc │ └── RoutingTests.kt ├── ktor-swagger-ui ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── smiley4 │ │ └── ktorswaggerui │ │ ├── PropertyBuilder.kt │ │ ├── ResourceContent.kt │ │ ├── SwaggerUI.kt │ │ ├── SwaggerUIRouteSelector.kt │ │ └── config │ │ ├── OperationsSort.kt │ │ ├── SwaggerUIConfig.kt │ │ ├── SwaggerUISyntaxHighlight.kt │ │ └── TagSort.kt │ └── test │ └── kotlin │ └── io │ └── github │ └── smiley4 │ └── ktorswaggerui │ └── RoutingTests.kt ├── mkdocs.yml └── settings.gradle.kts /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: [ push, pull_request, workflow_dispatch ] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up JDK 11 14 | uses: actions/setup-java@v3 15 | with: 16 | java-version: '11' 17 | distribution: 'temurin' 18 | cache: 'gradle' 19 | - name: Build with Gradle 20 | uses: gradle/gradle-build-action@v2.7.0 21 | with: 22 | arguments: build 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: workflow_dispatch 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | if: github.repository == 'SMILEY4/ktor-openapi-tools' 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: '11' 21 | distribution: 'temurin' 22 | cache: 'gradle' 23 | 24 | - name: Build 25 | uses: gradle/gradle-build-action@v2.7.0 26 | with: 27 | arguments: build 28 | 29 | - name: Publish 30 | run: | 31 | ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache 32 | ./gradlew releaseRepository 33 | env: 34 | SONATYPE_CONNECT_TIMEOUT_SECONDS: 180 35 | SONATYPE_CLOSE_TIMEOUT_SECONDS: 900 36 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 37 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 38 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_SECRET_KEY }} 39 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} 40 | -------------------------------------------------------------------------------- /.github/workflows/publish_docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docs 2 | 3 | on: workflow_dispatch 4 | 5 | permissions: write-all 6 | 7 | jobs: 8 | deployment: 9 | 10 | runs-on: ubuntu-latest 11 | if: github.repository == 'SMILEY4/ktor-openapi-tools' 12 | 13 | steps: 14 | 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: '11' 21 | distribution: 'temurin' 22 | cache: 'gradle' 23 | 24 | - name: Cache Python 25 | uses: actions/cache@v4 26 | env: 27 | cache-name: "cache-python" 28 | with: 29 | path: '.gradle/python' 30 | key: 'python' 31 | 32 | - name: Dokka 33 | run: ./gradlew dokkaHtml -Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m 34 | 35 | - name: Publish 36 | run: ./gradlew mkdocsPublish -Porg.ajoberstar.grgit.auth.username=${{ secrets.GRGIT_USER }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Jetbrains 2 | .idea/ 3 | 4 | ### Kotlin 5 | .kotlin 6 | *.class 7 | *.log 8 | *.jar 9 | 10 | # Dokka 11 | docs/dokka/ 12 | 13 | ### Visual Studio Code 14 | .vscode/ 15 | 16 | # Local History for Visual Studio Code 17 | .history/ 18 | 19 | ### Linux 20 | *~ 21 | 22 | # temporary files which can be created if a process still has a handle open of a deleted file 23 | .fuse_hidden* 24 | 25 | # KDE directory preferences 26 | .directory 27 | 28 | # Linux trash folder which might appear on any partition or disk 29 | .Trash-* 30 | 31 | # .nfs files are created when an open file is removed but is still being accessed 32 | .nfs* 33 | 34 | ### Windows 35 | # Windows thumbnail cache files 36 | Thumbs.db 37 | Thumbs.db:encryptable 38 | ehthumbs.db 39 | ehthumbs_vista.db 40 | 41 | # Dump file 42 | *.stackdump 43 | 44 | # Folder config file 45 | [Dd]esktop.ini 46 | 47 | # Recycle Bin used on file shares 48 | $RECYCLE.BIN/ 49 | 50 | # Windows Installer files 51 | *.cab 52 | *.msi 53 | *.msix 54 | *.msm 55 | *.msp 56 | 57 | # Windows shortcuts 58 | *.lnk 59 | 60 | ### Gradle 61 | .gradle/ 62 | build/ 63 | 64 | # Ignore Gradle GUI config 65 | gradle-app.setting 66 | 67 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 68 | !gradle-wrapper.jar 69 | 70 | # Avoid ignore Gradle wrappper properties 71 | !gradle-wrapper.properties 72 | 73 | # Cache of project 74 | .gradletasknamecache 75 | 76 | # Eclipse Gradle plugin generated files 77 | # Eclipse Core 78 | .project 79 | # JDT-specific (Eclipse Java Development Tools) 80 | .classpath 81 | 82 | ### macOS 83 | # General 84 | .DS_Store 85 | .AppleDouble 86 | .LSOverride 87 | 88 | # Icon must end with two \r 89 | Icon 90 | 91 | # Thumbnails 92 | ._* 93 | 94 | # Files that might appear in the root of a volume 95 | .DocumentRevisions-V100 96 | .fseventsd 97 | .Spotlight-V100 98 | .TemporaryItems 99 | .Trashes 100 | .VolumeIcon.icns 101 | .com.apple.timemachine.donotpresent 102 | 103 | # Directories potentially created on remote AFP share 104 | .AppleDB 105 | .AppleDesktop 106 | Network Trash Folder 107 | Temporary Items 108 | .apdisk 109 | 110 | 111 | ### Python 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # Installer logs 118 | pip-log.txt 119 | pip-delete-this-directory.txt 120 | 121 | # pyenv 122 | .python-version 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # mkdocs documentation 134 | /site 135 | .cache -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "2.0.21" 3 | id("org.jetbrains.dokka") version "1.9.20" apply false 4 | id("org.owasp.dependencycheck") version "8.2.1" apply false 5 | id("io.gitlab.arturbosch.detekt") version "1.23.0" apply false 6 | id("com.vanniktech.maven.publish") version "0.28.0" apply false 7 | id("com.github.ben-manes.versions") version "0.51.0" apply false 8 | id("ru.vyarus.mkdocs") version "4.0.1" 9 | } 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | mkdocs { 16 | sourcesDir = "." 17 | buildDir = "./build/mkdocs" 18 | updateSiteUrl = true 19 | publish { 20 | branch = "gh-pages" 21 | version = "5.x" 22 | rootRedirect = true 23 | rootRedirectTo = "latest" 24 | setVersionAliases("latest") 25 | generateVersionsFile = true 26 | } 27 | python { 28 | minPythonVersion = "3.12" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/assets/icons/redoc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/assets/images/redoc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SMILEY4/ktor-openapi-tools/a65a2c92d72654145dfe4a413bf94d52d516b17c/docs/assets/images/redoc.png -------------------------------------------------------------------------------- /docs/assets/images/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SMILEY4/ktor-openapi-tools/a65a2c92d72654145dfe4a413bf94d52d516b17c/docs/assets/images/swagger.png -------------------------------------------------------------------------------- /docs/assets/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | /*Make code in "details" a bit larger for better readability*/ 2 | details code { 3 | font-size: 1em !important; 4 | } 5 | 6 | /*Draw a light border around tab panels*/ 7 | @media screen and (max-width: 44.9844em) { 8 | .tabbed-set { 9 | border-bottom: 1px solid var(--md-default-fg-color--lightest); 10 | } 11 | } 12 | 13 | @media not screen and (max-width: 44.9844em) { 14 | .tabbed-set { 15 | border: 1px solid var(--md-default-fg-color--lightest); 16 | border-radius: .2rem !important; 17 | padding: 0 .3rem .3rem .3rem !important; 18 | } 19 | } -------------------------------------------------------------------------------- /docs/examples/authentication.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Authentication 7 | 8 | An example showing the handling of protected routes. 9 | 10 | ```kotlin title="Authentication.kt" linenums="1" 11 | ---8<--- "Authentication.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/basics.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Basic 7 | 8 | This example shows a basic usage of the plugin 9 | 10 | ```kotlin title="Basics.kt" linenums="1" 11 | --8<---- "Basics.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/complete-configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Complete Configuration 7 | 8 | This example showcases a *nearly* complete - and mostly nonsensical - plugin configuration 9 | 10 | ```kotlin title="CompleteConfig.kt" linenums="1" 11 | --8<-- "CompleteConfig.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/customized-schema-generator.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Schema Generation 7 | 8 | Example showcasing possible configurations for customizing the automatic schema generation. 9 | 10 | ```kotlin linenums="1" 11 | ---8<--- "CustomizedSchemaGenerator.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/external-openapi-spec.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # External OpenAPI Spec 7 | 8 | Example showcasing the usage of an external OpenAPI specification with Swagger UI and ReDoc. 9 | 10 | ```kotlin linenums="1" 11 | ---8<--- "ExternalOpenApiSpec.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/file-upload.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # File Upload 7 | 8 | An example showcasing the documentation of file upload routes and multipart bodies. 9 | 10 | ```kotlin linenums="1" 11 | ---8<--- "FileUpload.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Examples 7 | 8 | Runnable examples showcasing `ktor-openapi`, `ktor-swagger-ui` and `ktor-redoc` functionalities. -------------------------------------------------------------------------------- /docs/examples/kotlinx-serialization.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Kotlinx Serialization 7 | 8 | Example focusing on the usage of [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) with OpenAPI specification generation. 9 | 10 | ```kotlin linenums="1" 11 | ---8<--- "KotlinxSerialization.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/multiple-specs.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Multiple Specs 7 | 8 | An example showing the setup with multiple OpenAPI specification in a single application. 9 | 10 | ```kotlin linenums="1" 11 | ---8<--- "MultipleSpecs.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/openapi-examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Examples 7 | 8 | An example showing different ways to handle examples. 9 | 10 | ```kotlin linenums="1" 11 | ---8<--- "Examples.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/petstore.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Petstore 7 | 8 | An implementation of the "[Petstore](https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v2.0/json/petstore-simple.json)" sample project. 9 | 10 | ```kotlin linenums="1" 11 | ---8<--- "Petstore.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/request-response.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Requests and Responses 7 | 8 | An example focusing on documenting requests and responses. 9 | 10 | ```kotlin linenums="1" 11 | ---8<--- "RequestResponse.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/schemas.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Schemas 7 | 8 | An example showing different ways to handle schemas. 9 | 10 | ```kotlin linenums="1" 11 | ---8<--- "Schemas.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/webhooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | exclude: true 4 | --- 5 | 6 | # Webhooks 7 | 8 | This example showcases the documentation of webhooks. 9 | 10 | ```kotlin title="Webhooks.kt" linenums="1" 11 | --8<---- "Webhooks.kt" 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - title 4 | - navigation 5 | - toc 6 | - footer 7 | --- 8 | 9 | # Ktor OpenAPI Tools 10 | 11 | A collection of libraries to simplify API documentation and exploration for [Ktor](https://ktor.io/) applications. 12 | Designed to be non-invasive, they integrate seamlessly with applications without requiring immediate change to existing code while being highly customizable to fit every use case. 13 | 14 |
15 | 16 | - :simple-openapiinitiative:{ .lg .middle } __OpenAPI__ 17 | 18 | --- 19 | 20 | `ktor-openapi` 21 | 22 | Add documentation to Ktor routes and automatically generate [OpenAPI](https://www.openapis.org/) specifications. 23 | 24 | [:octicons-arrow-right-24: Get Started](openapi/index.md) 25 | 26 | 27 | - :simple-swagger:{ .lg .middle } __Swagger UI__ 28 | 29 | --- 30 | 31 | `ktor-swagger-ui` 32 | 33 | Serve an interactive [Swagger UI](https://swagger.io/tools/swagger-ui/) for easy API exploration and testing 34 | 35 | [:octicons-arrow-right-24: Get Started](swaggerui/index.md) 36 | 37 | 38 | - :redoc:{ .lg .middle } __Redoc__ 39 | 40 | --- 41 | 42 | `ktor-redoc` 43 | 44 | Serve an interactive [ReDoc](https://github.com/Redocly/redoc) page for easy API exploration and testing 45 | 46 | [:octicons-arrow-right-24: Get Started](redoc/index.md) 47 | 48 |
-------------------------------------------------------------------------------- /docs/openapi/example_encoding.md: -------------------------------------------------------------------------------- 1 | # Example Encoding 2 | 3 | Example objects are automatically serialized (as json) and added to the OpenAPI specification. 4 | The encoding of example values can be customized in the plugin configuration. 5 | If no encoder is specified, the example value will be encoded by swagger. 6 | 7 | ## Pre-Defined Example Encoders 8 | 9 | === "Swagger" 10 | Explicitly use the internal swagger example encoder. 11 | 12 | ```kotlin 13 | install(OpenApi) { 14 | examples { 15 | encoder = ExampleEncoder.internal() 16 | } 17 | } 18 | ``` 19 | 20 | ??? info "More Information" 21 | 22 | [:octicons-arrow-right-24: API Reference](../dokka/ktor-openapi/ktor-openapi/io.github.smiley4.ktoropenapi.config/-example-encoder/internal.html) 23 | 24 | === "Kotlinx.Serialization" 25 | Use [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) to encode example values. 26 | 27 | ```kotlin 28 | install(OpenApi) { 29 | examples { 30 | encoder = ExampleEncoder.kotlinx() 31 | } 32 | } 33 | ``` 34 | 35 | ??? info "More Information" 36 | 37 | [:octicons-arrow-right-24: API Reference](../dokka/ktor-openapi/ktor-openapi/io.github.smiley4.ktoropenapi.config/-example-encoder/kotlinx.html) 38 | 39 | 40 | ## Custom Example Encoder 41 | 42 | The example encoder can be completely replaced by an own implementation. 43 | 44 | ```kotlin 45 | install(OpenApi) { 46 | examples { 47 | encoder { type, example -> //(1)! 48 | TOOD() //(2)! 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | 1. Input of the example encoding function is a `io.github.smiley4.ktoropenapi.config.TypeDescriptor` with information about the type/schema and the actual value of the example. 55 | 2. Encode/Transform the example value and return the result. 56 | 57 | ???+ example "Custom "toString()" Example Encoder" 58 | 59 | ```kotlin 60 | install(OpenApi) { 61 | examples { 62 | encoder { type, example -> 63 | example.toString() //(1)! 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | 1. Always encode and embed the example value as a raw string. -------------------------------------------------------------------------------- /docs/openapi/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Add Dependency 4 | 5 | To generate OpenAPI specifications, you need to include the `ktor-openapi` artifact in the build script. 6 | All artifacts are published to Maven Central. 7 | 8 | === "Gradle (Kotlin)" 9 | ```kotlin 10 | implementation("io.github.smiley4:ktor-openapi:$version") 11 | ``` 12 | 13 | === "Gradle" 14 | ```groovy 15 | implementation 'io.github.smiley4:ktor-openapi:$version' 16 | ``` 17 | 18 | === "Maven" 19 | ```xml 20 | 21 | io.github.smiley4 22 | ktor-openapi 23 | ${version} 24 | 25 | ``` 26 | 27 | ??? tip "Ktor Compatibility and Previous Versions" 28 | 29 | This project as been split into multiple projects starting with version 5.0.
Versions up to 5.0 are called `ktor-swagger-ui` instead of `ktor-openapi`. 30 | 31 | | Ktor | Plugin Version | Project Name | 32 | |------|----------------|-------------------| 33 | | 2.x | up to 3.x | `ktor-swagger-ui` | 34 | | 3.x | 4.x | `ktor-swagger-ui` | 35 | | 3.x | 5.x | `ktor-openapi` | 36 | 37 | ## Install OpenAPI 38 | 39 | ```kotlin 40 | install(OpenApi) { //(1)! 41 | //...(2) 42 | } 43 | ``` 44 | 45 | 1. Install the "OpenAPI" plugin to the application. 46 | 2. Add additional plugin configuration here. 47 | 48 | ??? info "More Information" 49 | 50 | [:octicons-arrow-right-24: Plugin Configuration](plugin_configuration.md) 51 | 52 | [:octicons-arrow-right-24: API Reference](../dokka/ktor-openapi/ktor-openapi/io.github.smiley4.ktoropenapi.config/-open-api-plugin-config/index.html) 53 | 54 | 55 | ## Exposing OpenAPI Specification 56 | 57 | ```kotlin 58 | routing { 59 | route("api.json") { //(1)! 60 | openApi() //(2)! 61 | } 62 | } 63 | ``` 64 | 65 | 1. Create a new route to expose the OpenAPI specification file at `api.json`. 66 | 2. Expose the OpenAPI specification. 67 | 68 | ??? info "More Information" 69 | 70 | [:octicons-arrow-right-24: Multiple OpenAPI Specifications](multiple_specs.md) 71 | 72 | [:octicons-arrow-right-24: API Reference](../dokka/ktor-openapi/ktor-openapi/io.github.smiley4.ktoropenapi/open-api.html) 73 | 74 | 75 | ## Documenting Routes 76 | 77 | ```kotlin 78 | import io.github.smiley4.ktoropenapi.get //(1)! 79 | 80 | get("hello", { //(2)! 81 | description = "A Hello-World route" //(3)! 82 | response { 83 | HttpStatusCode.OK to { //(4)! 84 | description = "A success response" 85 | body() //(5)! 86 | } 87 | } 88 | //... 89 | }) { 90 | call.respondText("Hello World!") //(6)! 91 | } 92 | ``` 93 | 94 | 1. Replace `io.ktor.server.routing.get` with `io.github.smiley4.ktoropenapi.get`. Same for other http methods. 95 | 2. Enrich `/hello` route with additional information. 96 | 3. Add a description to the route. 97 | 4. Document the different possible responses. 98 | 5. Specify the response body type. The schema for the type is generated automatically. 99 | 6. Handle requests as usual. 100 | 101 | ??? info "More Information" 102 | 103 | [:octicons-arrow-right-24: Documenting Routes](documenting_routes.md) 104 | 105 | [:octicons-arrow-right-24: Documentation with Type-safe Routing](typesafe_routing.md) 106 | 107 | [:octicons-arrow-right-24: API Reference](../dokka/ktor-openapi/ktor-openapi/io.github.smiley4.ktoropenapi.config/-route-config/index.html) 108 | -------------------------------------------------------------------------------- /docs/openapi/index.md: -------------------------------------------------------------------------------- 1 | # OpenAPI 2 | 3 | [Ktor](https://ktor.io/) plugin to automatically generate [OpenAPI](https://www.openapis.org/) specifications from routes. Additional information can be gradually added to existing routes without requiring major changes to existing code. 4 | 5 | 6 | ## Features 7 | 8 | - Extends existing Ktor DSL 9 | - No immediate change to code required 10 | - Support for [Type-safe routing](https://ktor.io/docs/server-resources.html) / Resources plugin 11 | - Document webhooks and (limited) options for server-sent events 12 | - Covers (almost) complete [OpenAPI 3.1.0 Specification](https://swagger.io/specification/) 13 | - Automatically generates json schemas from kotlin types 14 | - Out-of-the-box support for type parameters, inheritance, collections, etc 15 | - Usable with reflection or [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) 16 | - Supports [Jackson](https://github.com/FasterXML/jackson), [Swagger](https://github.com/swagger-api/swagger-core), [Javax](https://mvnrepository.com/artifact/javax.validation/validation-api) 17 | and [Jakarta](https://github.com/jakartaee/validation/tree/main) annotations 18 | - Highly configurable and customizable 19 | 20 | 21 | ## Example 22 | 23 | ```kotlin 24 | install(OpenApi) //(1)! 25 | 26 | routing { 27 | 28 | route("api.json") { 29 | openApi() //(2)! 30 | } 31 | 32 | get("example", { 33 | description = "An example route" //(3)! 34 | response { 35 | HttpStatusCode.OK to { 36 | description = "A success response" 37 | body() 38 | } 39 | } 40 | }) { 41 | call.respondText("Hello World!") //(4)! 42 | } 43 | } 44 | ``` 45 | 46 | 1. Install and configure the OpenAPI plugin. 47 | 2. Create a route to expose the OpenAPI specification file at `/api.json`. 48 | 3. Add (optional) information to the route, e.g. a description and responses and response bodies. 49 | 4. Handle requests as usual. -------------------------------------------------------------------------------- /docs/openapi/plugin_configuration.md: -------------------------------------------------------------------------------- 1 | # Plugin Configuration 2 | 3 | ???+ info "More Information" 4 | 5 | The complete list of all available plugin configuration options can be viewed in the [API Reference](../dokka/ktor-openapi/ktor-openapi/io.github.smiley4.ktoropenapi.config/-open-api-plugin-config/index.html). 6 | 7 | 8 | ## Automatically Assigning Tags 9 | 10 | Tags can either be assigned at the route documentation directly or via a mapping function in the plugin configuration. 11 | The specified function is called for each route and the returned tags are added to the route's documentation. 12 | 13 | ```kotlin 14 | install(OpenApi) { 15 | tags { 16 | tagGenerator = { url -> TODO() } 17 | } 18 | } 19 | ``` 20 | 21 | 22 | ## Ignoring Route Selectors 23 | 24 | Other Ktor plugins may add additional route selectors to routes that show up the OpenAPI specification as unwanted parts in the urls. 25 | These can be ignored by adding them to the `ignoredRouteSelectors` or `ignoredRouteSelectorClassNames` configuration. 26 | 27 | ???+ example "Ignoring Route Selector" 28 | 29 | ```kotlin 30 | install(OpenApi) { 31 | tags { 32 | ignoredRouteSelectors = ignoredRouteSelectors + RateLimitRouteSelector::class//(1)! 33 | ignoredRouteSelectorClassNames = ignoredRouteSelectorClassNames + "io.ktor.server.plugins.ratelimit.RateLimitRouteSelector"//(2)! 34 | } 35 | } 36 | ``` 37 | 38 | 1. Ignore the route selector from the Ktor [Rate Limiting Plugin](https://ktor.io/docs/server-rate-limit.html) by adding its class to the ignored selectors. 39 | 2. Ignore the route selector from the Ktor [Rate Limiting Plugin](https://ktor.io/docs/server-rate-limit.html) by adding its qualified name to the ignored selector names. This can be useful when the actual class is internal and cannot be accessed. 40 | 41 | ## Excluding Routes 42 | 43 | Routes can be completely excluded from the generated OpenAPI specification via the `hidden`-flag at the route documentation or the `pathFilter`-function in the plugin configuration. 44 | 45 | ???+ example "Excluding Routes" 46 | 47 | === "Route Documentation" 48 | ```kotlin 49 | routing { 50 | get({ 51 | hidden = true //(1)! 52 | }) { 53 | call.respond(HttpStatusCode.NotImplemented, Unit) 54 | } 55 | } 56 | ``` 57 | 58 | 1. Mark this route as "hidden" and exclude it from the generated OpenAPI specification. 59 | 60 | === "Plugin Configuration" 61 | 62 | ```kotlin 63 | install(OpenApi) { 64 | pathFilter = { method, url -> url.firstOrNull() != "internal" }//(1)! 65 | } 66 | ``` 67 | 68 | 1. Filter out and exclude all routes with urls starting with "internal". The function receives the http method and url as inputs and returns `false` for excluded routes. 69 | 70 | ## Choosing the output format - JSON or YAML 71 | 72 | The output format of the generated OpenAPI specification can be changed from `json` to `yaml` with the `outputFormat` option. 73 | 74 | ???+ example "Choosing the Output Format" 75 | 76 | === "JSON" 77 | ```kotlin 78 | install(OpenApi) { 79 | outputFormat = OutputFormat.JSON 80 | } 81 | ``` 82 | 83 | === "YAML" 84 | 85 | ```kotlin 86 | install(OpenApi) { 87 | outputFormat = OutputFormat.YAML 88 | } 89 | ``` 90 | 91 | ## Customizing Schemas 92 | 93 | [:octicons-arrow-right-24: Schema Generation](schema_generation.md) 94 | 95 | ## Customizing Examples 96 | 97 | [:octicons-arrow-right-24: Example Encoding](example_encoding.md) 98 | 99 | ## Configuring Multiple OpenAPI Specifications 100 | 101 | [:octicons-arrow-right-24: Multiple API Specifications](multiple_specs.md) 102 | -------------------------------------------------------------------------------- /docs/openapi/typesafe_routing.md: -------------------------------------------------------------------------------- 1 | # Type-safe Routing 2 | 3 | This project supports documenting Ktor routes defined using [Type-safe Routing](https://ktor.io/docs/server-resources.html). 4 | 5 | ???+ example "Documenting Routes" 6 | 7 | ```kotlin 8 | @Resource("{id}") 9 | class Entity( //(1)! 10 | val id: String 11 | ) 12 | ``` 13 | 14 | 1. Define resources classes as usual. 15 | 16 | ```kotlin 17 | import io.github.smiley4.ktoropenapi.resources.get //(1)! 18 | 19 | routing { 20 | get({ //(2)! 21 | description = "Returns the entity with the requested id." 22 | }) { 23 | call.respond(HttpStatusCode.NotImplemented, Unit) //(3)! 24 | } 25 | } 26 | ``` 27 | 28 | 1. Replace `io.ktor.server.resources.get` with `io.github.smiley4.ktoropenapi.resources.get`. Same for other http methods. 29 | 2. Define the route using the resource class and add additional documentation. 30 | 3. Handle requests as usual. 31 | 32 | ??? info "More Information" 33 | 34 | See [Documenting Routes](documenting_routes.md) for more general information. 35 | 36 | ??? tip "Extracting Information from Resource Classes" 37 | 38 | When using type-safe routing, information about the routes can be extracted from the resources classes. 39 | This includes available query and path parameters with their names, types and whether they are optional. 40 | All other information still needs to be added manually and at the routes. 41 | 42 | To enable automatic extraction, set the `autoDocumentResourcesRoutes` option in the plugin configuration. 43 | 44 | ```kotlin 45 | install(OpenApi) { 46 | autoDocumentResourcesRoutes = true 47 | } 48 | ``` 49 | 50 | ??? warning "Schema Generation" 51 | 52 | When using Type-safe routing and `autoDocumentResourcesRoutes` is enabled, schemas must be generated using **kotlinx.serialization**. 53 | See [Schema Generation](schema_generation.md) for more information on how to change the default generator. 54 | 55 | ```kotlin 56 | install(OpenApi) { 57 | // enable automatically extracting documentation from resources-routes 58 | autoDocumentResourcesRoutes = true 59 | // schema-generator must use kotlinx-serialization to be compatible 60 | schemas { 61 | generator = SchemaGenerator.kotlinx() 62 | } 63 | } 64 | ``` 65 | 66 | -------------------------------------------------------------------------------- /docs/redoc/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Add Dependency 4 | 5 | To serve ReDoc, you need to include the `ktor-redoc` artifact in the build script. 6 | All artifacts are published to Maven Central. 7 | 8 | === "Gradle (Kotlin)" 9 | ```kotlin 10 | implementation("io.github.smiley4:ktor-redoc:$version") 11 | ``` 12 | 13 | === "Gradle" 14 | ```groovy 15 | implementation 'io.github.smiley4:ktor-redoc:$version' 16 | ``` 17 | 18 | === "Maven" 19 | ```xml 20 | 21 | io.github.smiley4 22 | ktor-redoc 23 | ${version} 24 | 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```kotlin 30 | routing { 31 | 32 | route("redoc") { //(1)! 33 | redoc("/api.json") { //(2)! 34 | //...(3) 35 | } 36 | } 37 | 38 | } 39 | ``` 40 | 41 | 1. Specify route to serve ReDoc at `/redoc`. 42 | 2. Expose ReDoc showing the OpenAPI specification at `/api.json`. The url can be relative pointing to specification provided by this application or absolute pointing to an external resource. 43 | 3. Add configuration for this ReDoc "instance" here. 44 | 45 | ??? tip "Using ReDoc with [auto-generated](../openapi/index.md) OpenAPI specification" 46 | 47 | ```kotlin 48 | routing { 49 | 50 | route("api.json") { 51 | openApi() //(1)! 52 | } 53 | route("redoc") { 54 | redoc("/api.json") //(2)! 55 | } 56 | 57 | } 58 | ``` 59 | 60 | 1. Serve auto-generated OpenAPI specification at `/api.json`. 61 | 2. Expose ReDoc using auto-generated specification at `/api.json`. 62 | 63 | ??? info "Configuration Options" 64 | 65 | For more information on available configuration options, please see the [api reference](../dokka/ktor-redoc/ktor-redoc/io.github.smiley4.ktorredoc.config/-redoc-config/index.html). -------------------------------------------------------------------------------- /docs/redoc/index.md: -------------------------------------------------------------------------------- 1 | # ReDoc 2 | 3 | Library for [Ktor](https://ktor.io/) applications to serve [ReDoc](https://github.com/Redocly/redoc) - visualize and interact with generated OpenAPI specifications. 4 | 5 | ## Features 6 | 7 | - Explore and interact with OpenAPI specifications generated by [`ktor-openapi`](../openapi/index.md) or external specifications 8 | - Serve bundled ReDoc page 9 | - Expose multiple "instances" of ReDoc (e.g. for different OpenAPI specifications) 10 | - All ReDoc configuration options available 11 | 12 | ## Example 13 | 14 | ```kotlin 15 | routing { 16 | 17 | route("redoc") { //(1)! 18 | redoc("/api.json") { //(2)! 19 | //...(3) 20 | } 21 | } 22 | 23 | } 24 | ``` 25 | 26 | 1. Specify route to serve ReDoc at `/redoc`. 27 | 2. Expose ReDoc showing OpenAPI specification at `/api.json`. Path can be relative pointing to specification provided by this application or absolute pointing to an external resource. 28 | 3. Add configuration for this ReDoc "instance" here. -------------------------------------------------------------------------------- /docs/swaggerui/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Add Dependency 4 | 5 | To serve Swagger UI, you need to include the `ktor-swagger-ui` artifact in the build script. 6 | All artifacts are published to Maven Central. 7 | 8 | === "Gradle (Kotlin)" 9 | ```kotlin 10 | implementation("io.github.smiley4:ktor-swagger-ui:$version") 11 | ``` 12 | 13 | === "Gradle" 14 | ```groovy 15 | implementation 'io.github.smiley4:ktor-swagger-ui:$version' 16 | ``` 17 | 18 | === "Maven" 19 | ```xml 20 | 21 | io.github.smiley4 22 | ktor-swagger-ui 23 | ${version} 24 | 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```kotlin 30 | routing { 31 | 32 | route("swagger") { //(1)! 33 | swaggerUI("/api.json") { //(2)! 34 | //...(3) 35 | } 36 | } 37 | 38 | } 39 | ``` 40 | 41 | 1. Specify route to serve Swagger UI at `/swagger`. 42 | 2. Expose Swagger UI showing OpenAPI specification at `/api.json`. The url can be relative pointing to specification provided by this application or absolute pointing to an external resource. Specify a map of urls to interact with multiple different specifications in the same Swagger UI "instance". 43 | 3. Add configuration for this Swagger UI "instance" here. 44 | 45 | ??? tip "Using Swagger UI with [auto-generated](../openapi/index.md) OpenAPI specification" 46 | 47 | ```kotlin 48 | routing { 49 | 50 | route("api.json") { 51 | openApi() //(1)! 52 | } 53 | route("swagger") { 54 | swaggerUI("/api.json") //(2)! 55 | } 56 | 57 | } 58 | ``` 59 | 60 | 1. Serve auto-generated OpenAPI specification at `/api.json`. 61 | 2. Expose Swagger UI using auto-generated specification at `/api.json`. 62 | 63 | 64 | ??? info "Configuration Options" 65 | 66 | For more information on available configuration options, please see the [api reference](../dokka/ktor-swagger-ui/ktor-swagger-ui/io.github.smiley4.ktorswaggerui.config/-swagger-u-i-config/index.html). -------------------------------------------------------------------------------- /docs/swaggerui/index.md: -------------------------------------------------------------------------------- 1 | # Swagger UI 2 | 3 | Library for [Ktor](https://ktor.io/) applications to serve [Swagger UI](https://swagger.io/tools/swagger-ui/) - visualize and interact with generated OpenAPI specifications. 4 | 5 | ## Features 6 | 7 | - Explore and interact with OpenAPI specifications generated by [`ktor-openapi`](../openapi/index.md) or external specifications 8 | - Serve bundled Swagger UI 9 | - Expose multiple "instances" of Swagger UI (e.g. for different OpenAPI specifications) 10 | - All Swagger UI configuration options available 11 | 12 | ## Example 13 | 14 | ```kotlin 15 | routing { 16 | 17 | route("swagger") { //(1)! 18 | swaggerUI("/api.json") { //(2)! 19 | //...(3) 20 | } 21 | } 22 | 23 | } 24 | ``` 25 | 26 | 1. Specify route to serve Swagger UI at `/swagger`. 27 | 2. Expose Swagger UI using OpenAPI specification at `/api.json`. Path can be relative pointing to specification provided by this application or absolute pointing to an external resource. 28 | 3. Add configuration for this Swagger UI "instance" here. -------------------------------------------------------------------------------- /examples/build.gradle.kts: -------------------------------------------------------------------------------- 1 | val projectGroupId: String by project 2 | val projectVersion: String by project 3 | group = projectGroupId 4 | version = projectVersion 5 | 6 | plugins { 7 | kotlin("jvm") 8 | kotlin("plugin.serialization") version "2.0.21" 9 | } 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation(project(":ktor-openapi")) 17 | implementation(project(":ktor-swagger-ui")) 18 | implementation(project(":ktor-redoc")) 19 | 20 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") 21 | 22 | val versionKtor: String by project 23 | implementation("io.ktor:ktor-server-netty-jvm:$versionKtor") 24 | implementation("io.ktor:ktor-server-content-negotiation:$versionKtor") 25 | implementation("io.ktor:ktor-serialization-jackson:$versionKtor") 26 | implementation("io.ktor:ktor-serialization-kotlinx-json:$versionKtor") 27 | 28 | 29 | implementation("io.ktor:ktor-server-auth:$versionKtor") 30 | implementation("io.ktor:ktor-server-call-logging:$versionKtor") 31 | implementation("io.ktor:ktor-server-test-host:$versionKtor") 32 | implementation("io.ktor:ktor-server-resources:$versionKtor") 33 | 34 | val versionSchemaKenerator: String by project 35 | implementation("io.github.smiley4:schema-kenerator-core:$versionSchemaKenerator") 36 | implementation("io.github.smiley4:schema-kenerator-reflection:$versionSchemaKenerator") 37 | implementation("io.github.smiley4:schema-kenerator-serialization:$versionSchemaKenerator") 38 | implementation("io.github.smiley4:schema-kenerator-swagger:$versionSchemaKenerator") 39 | implementation("io.github.smiley4:schema-kenerator-jackson:$versionSchemaKenerator") 40 | 41 | val versionSwaggerParser: String by project 42 | implementation("io.swagger.parser.v3:swagger-parser:$versionSwaggerParser") 43 | 44 | val versionKotlinLogging: String by project 45 | implementation("io.github.oshai:kotlin-logging-jvm:$versionKotlinLogging") 46 | 47 | val versionLogback: String by project 48 | implementation("ch.qos.logback:logback-classic:$versionLogback") 49 | } 50 | 51 | kotlin { 52 | jvmToolchain(11) 53 | } 54 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/Basics.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.examples 2 | 3 | import io.github.smiley4.ktoropenapi.OpenApi 4 | import io.github.smiley4.ktoropenapi.config.OutputFormat 5 | import io.github.smiley4.ktoropenapi.get 6 | import io.github.smiley4.ktoropenapi.openApi 7 | import io.github.smiley4.ktorredoc.redoc 8 | import io.github.smiley4.ktorswaggerui.swaggerUI 9 | import io.ktor.http.HttpStatusCode 10 | import io.ktor.server.application.Application 11 | import io.ktor.server.application.install 12 | import io.ktor.server.engine.embeddedServer 13 | import io.ktor.server.netty.Netty 14 | import io.ktor.server.response.respondText 15 | import io.ktor.server.routing.route 16 | import io.ktor.server.routing.routing 17 | 18 | fun main() { 19 | embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) 20 | } 21 | 22 | private fun Application.myModule() { 23 | 24 | // Install and configure the "OpenApi" Plugin 25 | install(OpenApi) { 26 | 27 | outputFormat = OutputFormat.JSON 28 | 29 | // configure basic information about the api 30 | info { 31 | title = "Example API" 32 | description = "An example api to showcase basic swagger-ui functionality." 33 | } 34 | // provide a reference to an external documentation 35 | externalDocs { 36 | url = "https://github.com/SMILEY4/ktor-swagger-ui/wiki" 37 | description = "Sample external documentation" 38 | } 39 | // configure the servers from where the api is being served 40 | server { 41 | url = "http://localhost:8080" 42 | description = "Development Server" 43 | } 44 | server { 45 | url = "https://www.example.com" 46 | description = "Production Server" 47 | } 48 | } 49 | 50 | routing { 51 | 52 | // Create a route for the openapi spec file. 53 | // This route will not be included in the spec. 54 | route("api.json") { 55 | openApi() 56 | } 57 | // Create a route for the Swagger UI using the openapi spec at "/api.json". 58 | // This route will not be included in the spec. 59 | route("swagger") { 60 | swaggerUI("/api.json") 61 | } 62 | // Create a route for redoc using the OpenAPI spec at "/api.json". 63 | // This route will not be included in the spec. 64 | route("redoc") { 65 | redoc("/api.json") 66 | } 67 | 68 | // a documented route 69 | get("hello", { 70 | // description of the route 71 | description = "A Hello-World route" 72 | // information about the request 73 | request { 74 | // information about the query parameter "name" of type "string" 75 | queryParameter("name") { 76 | description = "the name to greet" 77 | } 78 | } 79 | // information about possible responses 80 | response { 81 | // information about a "200 OK" response 82 | code(HttpStatusCode.OK) { 83 | // a description of the response 84 | description = "successful request - always returns 'Hello World!'" 85 | } 86 | } 87 | }) { 88 | call.respondText("Hello ${call.request.queryParameters["name"]}") 89 | } 90 | 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/CustomizedSchemaGenerator.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.examples 2 | 3 | import io.github.smiley4.ktoropenapi.OpenApi 4 | import io.github.smiley4.ktoropenapi.config.SchemaGenerator 5 | import io.github.smiley4.ktoropenapi.get 6 | import io.github.smiley4.ktoropenapi.openApi 7 | import io.github.smiley4.ktorredoc.redoc 8 | import io.github.smiley4.ktorswaggerui.swaggerUI 9 | import io.github.smiley4.schemakenerator.serialization.SerializationSteps.analyzeTypeUsingKotlinxSerialization 10 | import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.RequiredHandling 11 | import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.compileReferencingRoot 12 | import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.generateSwaggerSchema 13 | import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.withTitle 14 | import io.github.smiley4.schemakenerator.swagger.data.TitleType 15 | import io.ktor.http.HttpStatusCode 16 | import io.ktor.server.application.Application 17 | import io.ktor.server.application.install 18 | import io.ktor.server.engine.embeddedServer 19 | import io.ktor.server.netty.Netty 20 | import io.ktor.server.response.respond 21 | import io.ktor.server.routing.route 22 | import io.ktor.server.routing.routing 23 | import kotlinx.serialization.Serializable 24 | 25 | fun main() { 26 | embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) 27 | } 28 | 29 | private fun Application.myModule() { 30 | 31 | // Install and configure the OpenApi plugin 32 | install(OpenApi) { 33 | schemas { 34 | // replace default schema-generator with configurable pre-defined generator, or ... 35 | generator = SchemaGenerator.kotlinx { 36 | nullables = RequiredHandling.NON_REQUIRED 37 | optionals = RequiredHandling.REQUIRED 38 | title = TitleType.SIMPLE 39 | explicitNullTypes = false 40 | } 41 | // ... replace default schema-generator with completely custom generator 42 | generator = { type -> 43 | type 44 | .analyzeTypeUsingKotlinxSerialization() 45 | .generateSwaggerSchema { 46 | nullables = RequiredHandling.NON_REQUIRED 47 | optionals = RequiredHandling.REQUIRED 48 | } 49 | .withTitle(TitleType.SIMPLE) 50 | .compileReferencingRoot( 51 | explicitNullTypes = false 52 | ) 53 | } 54 | } 55 | } 56 | 57 | routing { 58 | 59 | // add the routes for the OpenAPI spec, Swagger UI and ReDoc 60 | route("swagger") { 61 | swaggerUI("/api.json") 62 | } 63 | route("api.json") { 64 | openApi() 65 | } 66 | route("redoc") { 67 | redoc("/api.json") 68 | } 69 | 70 | // a documented route 71 | get("hello", { 72 | // information about the request 73 | response { 74 | // information about a "200 OK" response 75 | code(HttpStatusCode.OK) { 76 | // body of the response 77 | body() 78 | } 79 | } 80 | }) { 81 | call.respond(HttpStatusCode.NotImplemented, "...") 82 | } 83 | 84 | } 85 | 86 | } 87 | 88 | @Serializable 89 | private class MyResponseBody( 90 | val name: String, 91 | ) 92 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/ExternalOpenApiSpec.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.examples 2 | 3 | import io.github.smiley4.ktorredoc.redoc 4 | import io.github.smiley4.ktorswaggerui.swaggerUI 5 | import io.ktor.server.application.Application 6 | import io.ktor.server.engine.embeddedServer 7 | import io.ktor.server.netty.Netty 8 | import io.ktor.server.routing.route 9 | import io.ktor.server.routing.routing 10 | 11 | fun main() { 12 | embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) 13 | } 14 | 15 | private fun Application.myModule() { 16 | 17 | routing { 18 | 19 | // Create a route for the swagger ui using an external OpenAPI spec. 20 | route("swagger") { 21 | swaggerUI("https://petstore3.swagger.io/api/v3/openapi.json") 22 | } 23 | 24 | // Create a route for Redoc using an external OpenAPI spec. 25 | route("redoc") { 26 | redoc("https://petstore3.swagger.io/api/v3/openapi.json") 27 | } 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/FileUpload.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.examples 2 | 3 | import io.github.smiley4.ktoropenapi.OpenApi 4 | import io.github.smiley4.ktoropenapi.config.SchemaGenerator 5 | import io.github.smiley4.ktoropenapi.post 6 | import io.github.smiley4.ktoropenapi.openApi 7 | import io.github.smiley4.ktorredoc.redoc 8 | import io.github.smiley4.ktorswaggerui.swaggerUI 9 | import io.ktor.http.ContentType 10 | import io.ktor.http.HttpStatusCode 11 | import io.ktor.server.application.Application 12 | import io.ktor.server.application.install 13 | import io.ktor.server.engine.embeddedServer 14 | import io.ktor.server.netty.Netty 15 | import io.ktor.server.response.respond 16 | import io.ktor.server.routing.route 17 | import io.ktor.server.routing.routing 18 | import java.io.File 19 | 20 | fun main() { 21 | embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) 22 | } 23 | 24 | private fun Application.myModule() { 25 | 26 | // Install the OpenAPI plugin and use the default configuration 27 | install(OpenApi) { 28 | schemas { 29 | // overwrite type "File" with custom schema for binary data 30 | generator = SchemaGenerator.reflection { 31 | overwrite(SchemaGenerator.TypeOverwrites.File()) 32 | } 33 | } 34 | } 35 | 36 | routing { 37 | 38 | // add the routes for the OpenAPI spec, Swagger UI and ReDoc 39 | route("swagger") { 40 | swaggerUI("/api.json") 41 | } 42 | route("api.json") { 43 | openApi() 44 | } 45 | route("redoc") { 46 | redoc("/api.json") 47 | } 48 | 49 | // upload a single file, either as png, jpeg or svg 50 | post("single", { 51 | request { 52 | body { 53 | mediaTypes( 54 | ContentType.Image.PNG, 55 | ContentType.Image.JPEG, 56 | ContentType.Image.SVG, 57 | ) 58 | } 59 | } 60 | }) { 61 | call.respond(HttpStatusCode.NotImplemented, "...") 62 | } 63 | 64 | // upload multiple files 65 | post("multipart", { 66 | request { 67 | multipartBody { 68 | mediaTypes(ContentType.MultiPart.FormData) 69 | part("first-image") { 70 | mediaTypes( 71 | ContentType.Image.PNG, 72 | ContentType.Image.JPEG, 73 | ContentType.Image.SVG 74 | ) 75 | } 76 | part("second-image") { 77 | mediaTypes( 78 | ContentType.Image.PNG, 79 | ContentType.Image.JPEG, 80 | ContentType.Image.SVG 81 | ) 82 | } 83 | } 84 | } 85 | }) { 86 | call.respond(HttpStatusCode.NotImplemented, "...") 87 | } 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/KotlinxSerialization.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package io.github.smiley4.ktoropenapi.examples 4 | 5 | import io.github.smiley4.ktoropenapi.OpenApi 6 | import io.github.smiley4.ktoropenapi.config.ExampleEncoder 7 | import io.github.smiley4.ktoropenapi.config.SchemaGenerator 8 | import io.github.smiley4.ktoropenapi.get 9 | import io.github.smiley4.ktoropenapi.openApi 10 | import io.github.smiley4.ktorredoc.redoc 11 | import io.github.smiley4.ktorswaggerui.swaggerUI 12 | import io.ktor.http.HttpStatusCode 13 | import io.ktor.server.application.Application 14 | import io.ktor.server.application.install 15 | import io.ktor.server.engine.embeddedServer 16 | import io.ktor.server.netty.Netty 17 | import io.ktor.server.response.respondText 18 | import io.ktor.server.routing.route 19 | import io.ktor.server.routing.routing 20 | import kotlinx.serialization.ExperimentalSerializationApi 21 | import kotlinx.serialization.Serializable 22 | import kotlinx.serialization.json.Json 23 | import kotlinx.serialization.json.JsonNamingStrategy 24 | 25 | fun main() { 26 | embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) 27 | } 28 | 29 | private fun Application.myModule() { 30 | 31 | val json = Json { 32 | prettyPrint = true 33 | encodeDefaults = true 34 | explicitNulls = false 35 | namingStrategy = JsonNamingStrategy.SnakeCase 36 | } 37 | 38 | install(OpenApi) { 39 | schemas { 40 | // configure the schema generator to use the default kotlinx-serializer 41 | generator = SchemaGenerator.kotlinx(json) 42 | } 43 | examples { 44 | // configure the example encoder to encode kotlin objects using kotlinx-serializer 45 | exampleEncoder = ExampleEncoder.kotlinx(json) 46 | } 47 | } 48 | 49 | routing { 50 | 51 | // add the routes for the OpenAPI spec, Swagger UI and ReDoc 52 | route("swagger") { 53 | swaggerUI("/api.json") 54 | } 55 | route("api.json") { 56 | openApi() 57 | } 58 | route("redoc") { 59 | redoc("/api.json") 60 | } 61 | 62 | // a documented route 63 | get("hello", { 64 | description = "A Hello-World route" 65 | request { 66 | queryParameter("name") { 67 | description = "the name to greet" 68 | example("Name Parameter") { 69 | value = "Mr. Example" 70 | } 71 | } 72 | } 73 | response { 74 | code(HttpStatusCode.OK) { 75 | description = "successful request - always returns 'Hello World!'" 76 | body { 77 | example("Success Response") { 78 | value = TestResponse( 79 | name = "Mr. Example", 80 | length = 11 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | }) { 87 | call.respondText("Hello ${call.request.queryParameters["name"]}") 88 | } 89 | 90 | } 91 | 92 | } 93 | 94 | 95 | @Serializable 96 | data class TestResponse( 97 | val name: String, 98 | val length: Int, 99 | ) 100 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/Minimal.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.examples 2 | 3 | import io.github.smiley4.ktoropenapi.OpenApi 4 | import io.github.smiley4.ktoropenapi.get 5 | import io.github.smiley4.ktoropenapi.openApi 6 | import io.github.smiley4.ktorredoc.redoc 7 | import io.github.smiley4.ktorswaggerui.swaggerUI 8 | import io.ktor.http.HttpStatusCode 9 | import io.ktor.server.application.Application 10 | import io.ktor.server.application.install 11 | import io.ktor.server.engine.embeddedServer 12 | import io.ktor.server.netty.Netty 13 | import io.ktor.server.response.respondText 14 | import io.ktor.server.routing.route 15 | import io.ktor.server.routing.routing 16 | 17 | fun main() { 18 | embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) 19 | } 20 | 21 | private fun Application.myModule() { 22 | 23 | // Install the OpenAPI plugin and use the default configuration 24 | install(OpenApi) 25 | 26 | routing { 27 | 28 | // Create a route for the OpenAPI spec file. 29 | // This route will not be included in the spec. 30 | route("api.json") { 31 | openApi() 32 | } 33 | // Create a route for the Swagger UI using the OpenAPI spec at "/api.json". 34 | // This route will not be included in the spec. 35 | route("swagger") { 36 | swaggerUI("/api.json") { 37 | 38 | } 39 | } 40 | // Create a route for ReDoc using the OpenAPI spec at "/api.json". 41 | // This route will not be included in the spec. 42 | route("redoc") { 43 | redoc("/api.json") 44 | } 45 | 46 | // a documented route 47 | get("hello", { 48 | // description of this route 49 | description = "A Hello-World route" 50 | response { 51 | HttpStatusCode.OK to { 52 | description = "A successful response" 53 | body() 54 | } 55 | } 56 | }) { 57 | call.respondText("Hello World!") 58 | } 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/MultipleSpecs.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.examples 2 | 3 | import io.github.smiley4.ktoropenapi.OpenApi 4 | import io.github.smiley4.ktoropenapi.get 5 | import io.github.smiley4.ktoropenapi.openApi 6 | import io.github.smiley4.ktoropenapi.route 7 | import io.github.smiley4.ktorredoc.redoc 8 | import io.github.smiley4.ktorswaggerui.swaggerUI 9 | import io.ktor.server.application.Application 10 | import io.ktor.server.application.install 11 | import io.ktor.server.engine.embeddedServer 12 | import io.ktor.server.netty.Netty 13 | import io.ktor.server.response.respondText 14 | import io.ktor.server.routing.route 15 | import io.ktor.server.routing.routing 16 | 17 | fun main() { 18 | embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) 19 | } 20 | 21 | private fun Application.myModule() { 22 | 23 | // Install and configure the OpenAPI plugin 24 | install(OpenApi) { 25 | // "global" configuration for all specs 26 | info { 27 | title = "Example API" 28 | } 29 | // configuration specific for spec "version1", overwrites global config 30 | spec("version1") { 31 | info { 32 | version = "1.0" 33 | } 34 | } 35 | // configuration specific for spec "version2", overwrites global config 36 | spec("version2") { 37 | info { 38 | version = "2.0" 39 | } 40 | } 41 | // assign all unassigned routes to spec "version2" (here only route '/greet') 42 | specAssigner = { _, _ -> "version2" } 43 | } 44 | 45 | routing { 46 | 47 | // add routes for "version1" spec, Swagger UI and ReDoc 48 | route("v1") { 49 | // OpenAPI spec containing all routes assigned to "version1" 50 | route("api.json") { 51 | openApi("version1") 52 | } 53 | // Swagger UI using '/v1/api.json' 54 | route("swagger") { 55 | swaggerUI("/v1/api.json") 56 | } 57 | // ReDoc using '/v1/api.json' 58 | route("redoc") { 59 | redoc("/v1/api.json") 60 | } 61 | } 62 | 63 | // add routes for "version2" spec, Swagger UI and ReDoc 64 | route("v2") { 65 | // OpenAPI spec containing all routes assigned to "version2" 66 | route("api.json") { 67 | openApi("version2") 68 | } 69 | // Swagger UI using '/v2/api.json' 70 | route("swagger") { 71 | swaggerUI("/v2/api.json") 72 | } 73 | // ReDoc using '/v2/api.json' 74 | route("redoc") { 75 | redoc("/v2/api.json") 76 | } 77 | } 78 | 79 | // version 1.0 routes 80 | route("v1", { 81 | specName = "version1" 82 | }) { 83 | 84 | // "hello" route in version 1.0 85 | get("hello", { 86 | description = "Version 1 'Hello World'" 87 | }) { 88 | call.respondText("Hello World!") 89 | } 90 | 91 | } 92 | 93 | // version 2.0 routes 94 | route("v2", { 95 | specName = "version2" 96 | }) { 97 | 98 | // "hello" route in version 2.0 99 | get("hello", { 100 | description = "Version 2 'Hello World'" 101 | }) { 102 | call.respondText("Hello World! (improved)") 103 | } 104 | 105 | } 106 | 107 | // unassigned route 108 | get("greet", { 109 | description = "Alternative route not manually assigned to any spec." 110 | }) { 111 | call.respondText("Alternative Hello World!") 112 | } 113 | 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/Webhooks.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.examples 2 | 3 | import io.github.smiley4.ktoropenapi.OpenApi 4 | import io.github.smiley4.ktoropenapi.openApi 5 | import io.github.smiley4.ktoropenapi.post 6 | import io.github.smiley4.ktoropenapi.webhook 7 | import io.github.smiley4.ktorredoc.redoc 8 | import io.github.smiley4.ktorswaggerui.swaggerUI 9 | import io.ktor.http.ContentType 10 | import io.ktor.http.HttpMethod 11 | import io.ktor.http.HttpStatusCode 12 | import io.ktor.server.application.Application 13 | import io.ktor.server.application.install 14 | import io.ktor.server.engine.embeddedServer 15 | import io.ktor.server.netty.Netty 16 | import io.ktor.server.response.respond 17 | import io.ktor.server.routing.route 18 | import io.ktor.server.routing.routing 19 | 20 | fun main() { 21 | embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) 22 | } 23 | 24 | private fun Application.myModule() { 25 | 26 | // Install the "OpenApi"-Plugin 27 | install(OpenApi) 28 | 29 | routing { 30 | 31 | // add the routes for the api-spec, swagger-ui and redoc 32 | route("api.json") { 33 | openApi() 34 | } 35 | route("swagger") { 36 | swaggerUI("/api.json") 37 | } 38 | route("redoc") { 39 | redoc("/api.json") 40 | } 41 | 42 | // a "normal" documented route to register for notifications 43 | post("registerForAlert", { 44 | description = "Register an URL to be called when new concerts are scheduled." 45 | request { 46 | body { 47 | description = "The URL to be notified about approaching concerts." 48 | } 49 | } 50 | }) { 51 | call.respond(HttpStatusCode.NotImplemented, Unit) 52 | } 53 | 54 | // documentation of the webhook to notify 55 | webhook(HttpMethod.Post, "concertAlert") { 56 | description = "Notify the registered URL with details of an upcoming concert" 57 | request { 58 | body { 59 | mediaTypes(ContentType.Text.Plain) 60 | required = true 61 | } 62 | } 63 | } 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /examples/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %cyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) | %highlight(%-5.5level{5}) | %gray(%-16.16thread{16}) | %magenta(%-25.25logger{25}) | %m%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | # project id 4 | projectGroupId=io.github.smiley4 5 | projectVersion=5.1.0 6 | 7 | # common publishing information 8 | projectBaseScmUrl=https://github.com/SMILEY4/ 9 | projectBaseScmConnection=scm:git:git://github.com/SMILEY4/ 10 | projectLicenseName=The Apache License, Version 2.0 11 | projectLicenseUrl=https://www.apache.org/licenses/LICENSE-2.0.txt 12 | projectDeveloperName=smiley4 13 | projectDeveloperUrl=https://github.com/SMILEY4 14 | 15 | # dependency versions 16 | versionKtor=3.1.1 17 | versionSwaggerUI=5.17.14 18 | versionSwaggerParser=2.1.24 19 | versionSchemaKenerator=2.1.2 20 | versionKotlinLogging=7.0.0 21 | versionKotest=5.8.0 22 | versionKotlinTest=2.0.21 23 | versionMockk=1.13.12 24 | versionLogback=1.5.6 25 | versionJackson=2.18.1 26 | versionRedoc=2.1.5 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SMILEY4/ktor-openapi-tools/a65a2c92d72654145dfe4a413bf94d52d516b17c/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.10.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/example/ExampleContext.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.example 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.ExampleDescriptor 4 | import io.swagger.v3.oas.models.examples.Example 5 | 6 | /** 7 | * Provides examples for an openapi-spec 8 | */ 9 | internal interface ExampleContext { 10 | 11 | /** 12 | * Get an [Example] (or a ref to an example) by its [ExampleDescriptor]. 13 | */ 14 | fun getExample(descriptor: ExampleDescriptor): Example 15 | 16 | 17 | /** 18 | * Get all examples placed in the components-section of the spec. 19 | */ 20 | fun getComponentSection(): Map 21 | } 22 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/ComponentsBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.OpenApiPluginData 4 | import io.swagger.v3.oas.models.Components 5 | import io.swagger.v3.oas.models.examples.Example 6 | import io.swagger.v3.oas.models.media.Schema 7 | 8 | /** 9 | * Builds the openapi [Components]-object containing shared reusable schemas and examples. 10 | * See [OpenAPI Specification - Components Object](https://swagger.io/specification/#components-object). 11 | */ 12 | internal class ComponentsBuilder( 13 | private val config: OpenApiPluginData, 14 | private val securitySchemesBuilder: SecuritySchemesBuilder 15 | ) { 16 | 17 | fun build(schemas: Map>, examples: Map): Components { 18 | return Components().also { 19 | it.schemas = schemas 20 | it.examples = examples 21 | if (config.securityConfig.securitySchemes.isNotEmpty()) { 22 | it.securitySchemes = securitySchemesBuilder.build(config.securityConfig.securitySchemes) 23 | } 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/ContactBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.ContactData 4 | import io.swagger.v3.oas.models.info.Contact 5 | 6 | /** 7 | * Builds the openapi [Contact]-object. Holds Contact information for the exposed API. 8 | * See [OpenAPI Specification - Contact Object](https://swagger.io/specification/#contact-object). 9 | */ 10 | internal class ContactBuilder { 11 | 12 | fun build(contact: ContactData): Contact = 13 | Contact().also { 14 | it.name = contact.name 15 | it.email = contact.email 16 | it.url = contact.url 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/ExternalDocumentationBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.ExternalDocsData 4 | import io.swagger.v3.oas.models.ExternalDocumentation 5 | 6 | /** 7 | * Build the openapi [ExternalDocumentation]-object. Allows referencing an external resource for extended documentation. 8 | * See [OpenAPI Specification - External Documentation Object](https://swagger.io/specification/#external-documentation-object). 9 | */ 10 | internal class ExternalDocumentationBuilder { 11 | 12 | fun build(externalDocs: ExternalDocsData): ExternalDocumentation = 13 | ExternalDocumentation().also { 14 | it.url = externalDocs.url 15 | it.description = externalDocs.description 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/HeaderBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.builder.schema.SchemaContext 4 | import io.github.smiley4.ktoropenapi.data.HeaderData 5 | import io.swagger.v3.oas.models.headers.Header 6 | 7 | /** 8 | * Build the openapi [Header]-object. 9 | * See [OpenAPI Specification - Header Object](https://swagger.io/specification/#header-object). 10 | */ 11 | internal class HeaderBuilder( 12 | private val schemaContext: SchemaContext 13 | ) { 14 | 15 | fun build(header: HeaderData): Header = 16 | Header().also { 17 | it.description = header.description 18 | it.required = header.required 19 | it.deprecated = header.deprecated 20 | it.schema = header.type?.let { t -> schemaContext.getSchema(t) } 21 | it.explode = header.explode 22 | // it.example = TODO() 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/InfoBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.InfoData 4 | import io.swagger.v3.oas.models.info.Info 5 | 6 | /** 7 | * Build the openapi [Info]-object. Holds metadata about the API. 8 | * See [OpenAPI Specification - Info Object](https://swagger.io/specification/#info-object). 9 | */ 10 | internal class InfoBuilder( 11 | private val contactBuilder: ContactBuilder, 12 | private val licenseBuilder: LicenseBuilder 13 | ) { 14 | 15 | fun build(info: InfoData): Info = 16 | Info().also { 17 | it.title = info.title 18 | it.version = info.version 19 | it.description = info.description 20 | it.termsOfService = info.termsOfService 21 | info.contact?.also { contact -> 22 | it.contact = contactBuilder.build(contact) 23 | } 24 | info.license?.also { license -> 25 | it.license = licenseBuilder.build(license) 26 | } 27 | it.summary = info.summary 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/LicenseBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.LicenseData 4 | import io.swagger.v3.oas.models.info.License 5 | 6 | /** 7 | * Build the openapi [License]-object. Holds license information for the exposed API. 8 | * See [OpenAPI Specification - License Object](https://swagger.io/specification/#license-object). 9 | */ 10 | internal class LicenseBuilder { 11 | 12 | fun build(license: LicenseData): License = 13 | License().also { 14 | it.name = license.name 15 | it.url = license.url 16 | it.identifier = license.identifier 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/OAuthFlowsBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.OpenIdOAuthFlowData 4 | import io.github.smiley4.ktoropenapi.data.OpenIdOAuthFlowsData 5 | import io.swagger.v3.oas.models.security.OAuthFlow 6 | import io.swagger.v3.oas.models.security.OAuthFlows 7 | import io.swagger.v3.oas.models.security.Scopes 8 | 9 | /** 10 | * Build the openapi [OAuthFlows]-object. Holds configuration of the supported OAuth Flows. 11 | * See [OpenAPI Specification - OAuth Flows Object](https://swagger.io/specification/#oauth-flows-object). 12 | */ 13 | internal class OAuthFlowsBuilder { 14 | 15 | fun build(flows: OpenIdOAuthFlowsData): OAuthFlows { 16 | return OAuthFlows().apply { 17 | implicit = flows.implicit?.let { build(it) } 18 | password = flows.password?.let { build(it) } 19 | clientCredentials = flows.clientCredentials?.let { build(it) } 20 | authorizationCode = flows.authorizationCode?.let { build(it) } 21 | } 22 | } 23 | 24 | private fun build(flow: OpenIdOAuthFlowData): OAuthFlow { 25 | return OAuthFlow().apply { 26 | authorizationUrl = flow.authorizationUrl 27 | tokenUrl = flow.tokenUrl 28 | refreshUrl = flow.refreshUrl 29 | scopes = flow.scopes?.let { buildScopes(it) } 30 | } 31 | } 32 | 33 | private fun buildScopes(scopes: Map): Scopes { 34 | return Scopes().apply { 35 | scopes.forEach { (k, v) -> addString(k, v) } 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/OpenApiBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.builder.example.ExampleContext 4 | import io.github.smiley4.ktoropenapi.data.OpenApiPluginData 5 | import io.github.smiley4.ktoropenapi.builder.route.RouteMeta 6 | import io.github.smiley4.ktoropenapi.builder.schema.SchemaContext 7 | import io.swagger.v3.oas.models.OpenAPI 8 | import io.swagger.v3.oas.models.SpecVersion 9 | 10 | /** 11 | * Build the openapi [OpenAPI]-object. Is the root of the openapi document. 12 | * See [OpenAPI Specification - OpenAPI Object](https://swagger.io/specification/#openapi-object). 13 | */ 14 | internal class OpenApiBuilder( 15 | private val config: OpenApiPluginData, 16 | private val schemaContext: SchemaContext, 17 | private val exampleContext: ExampleContext, 18 | private val infoBuilder: InfoBuilder, 19 | private val externalDocumentationBuilder: ExternalDocumentationBuilder, 20 | private val serverBuilder: ServerBuilder, 21 | private val tagBuilder: TagBuilder, 22 | private val pathsBuilder: PathsBuilder, 23 | private val webhooksBuilder: WebhooksBuilder, 24 | private val componentsBuilder: ComponentsBuilder, 25 | ) { 26 | 27 | fun build(routes: Collection): OpenAPI { 28 | return OpenAPI().also { 29 | it.specVersion = SpecVersion.V31 30 | it.openapi = "3.1.0" 31 | it.info = infoBuilder.build(config.info) 32 | it.externalDocs = externalDocumentationBuilder.build(config.externalDocs) 33 | it.servers = config.servers.map { server -> serverBuilder.build(server) } 34 | it.tags = config.tagsConfig.tags.map { tag -> tagBuilder.build(tag) } 35 | it.paths = pathsBuilder.build(routes.filter { r -> !r.isWebhook}) 36 | it.webhooks = webhooksBuilder.build(routes.filter { r -> r.isWebhook}) 37 | it.components = componentsBuilder.build(schemaContext.getComponentSection(), exampleContext.getComponentSection()) 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/OperationBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.builder.route.RouteMeta 4 | import io.swagger.v3.oas.models.Operation 5 | 6 | /** 7 | * Build the openapi [Operation]-object. Holds information describing a single API operation on a path. 8 | * See [OpenAPI Specification - Operation Object](https://swagger.io/specification/#operation-object). 9 | */ 10 | internal class OperationBuilder( 11 | private val operationTagsBuilder: OperationTagsBuilder, 12 | private val parameterBuilder: ParameterBuilder, 13 | private val requestBodyBuilder: RequestBodyBuilder, 14 | private val responsesBuilder: ResponsesBuilder, 15 | private val securityRequirementsBuilder: SecurityRequirementsBuilder, 16 | private val externalDocumentationBuilder: ExternalDocumentationBuilder, 17 | private val serverBuilder: ServerBuilder 18 | ) { 19 | 20 | fun build(route: RouteMeta): Operation = 21 | Operation().also { 22 | it.summary = route.documentation.summary 23 | it.description = route.documentation.description 24 | it.operationId = route.documentation.operationId 25 | it.deprecated = route.documentation.deprecated 26 | it.tags = operationTagsBuilder.build(route) 27 | it.parameters = route.documentation.request.parameters 28 | .filter { param -> !param.hidden } 29 | .map { param -> parameterBuilder.build(param) } 30 | route.documentation.request.body?.let { body -> 31 | it.requestBody = requestBodyBuilder.build(body) 32 | } 33 | it.responses = responsesBuilder.build(route.documentation.responses, route.protected) 34 | if (route.protected) { 35 | securityRequirementsBuilder.build(route).also { securityRequirements -> 36 | if (securityRequirements.isNotEmpty()) { 37 | it.security = securityRequirements 38 | } 39 | } 40 | } 41 | it.externalDocs = route.documentation.externalDocs?.let { docs -> externalDocumentationBuilder.build(docs) } 42 | if (route.documentation.servers.isNotEmpty()) { 43 | it.servers = route.documentation.servers.map { server -> serverBuilder.build(server) } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/OperationTagsBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.OpenApiPluginData 4 | import io.github.smiley4.ktoropenapi.builder.route.RouteMeta 5 | 6 | /** 7 | * Builds the list of tags for a single route. 8 | */ 9 | internal class OperationTagsBuilder( 10 | private val config: OpenApiPluginData 11 | ) { 12 | 13 | fun build(route: RouteMeta): List { 14 | return mutableSetOf().also { tags -> 15 | tags.addAll(getGeneratedTags(route)) 16 | tags.addAll(getRouteTags(route)) 17 | }.filterNotNull() 18 | } 19 | 20 | private fun getRouteTags(route: RouteMeta) = route.documentation.tags 21 | 22 | private fun getGeneratedTags(route: RouteMeta) = config.tagsConfig.generator(route.path.split("/").filter { it.isNotEmpty() }) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/ParameterBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.builder.example.ExampleContext 4 | import io.github.smiley4.ktoropenapi.builder.schema.SchemaContext 5 | import io.github.smiley4.ktoropenapi.data.RequestParameterData 6 | import io.github.smiley4.ktoropenapi.config.ParameterLocation 7 | import io.swagger.v3.oas.models.parameters.Parameter 8 | 9 | /** 10 | * Build the openapi [Parameter]-object. Holds information describing a single operation (query, path or header) parameter. 11 | * See [OpenAPI Specification - Parameter Object](https://swagger.io/specification/#parameter-object). 12 | */ 13 | internal class ParameterBuilder( 14 | private val schemaContext: SchemaContext, 15 | private val exampleContext: ExampleContext 16 | ) { 17 | 18 | fun build(parameter: RequestParameterData): Parameter = 19 | Parameter().also { 20 | it.`in` = when (parameter.location) { 21 | ParameterLocation.QUERY -> "query" 22 | ParameterLocation.HEADER -> "header" 23 | ParameterLocation.PATH -> "path" 24 | ParameterLocation.COOKIE -> "cookie" 25 | } 26 | it.name = parameter.name 27 | it.description = parameter.description 28 | it.required = parameter.required 29 | it.deprecated = parameter.deprecated 30 | it.allowEmptyValue = parameter.allowEmptyValue 31 | it.explode = parameter.explode 32 | it.example = parameter.example?.let { e -> exampleContext.getExample(e).value } 33 | it.allowReserved = parameter.allowReserved 34 | it.schema = schemaContext.getSchema(parameter.type) 35 | it.style = parameter.style 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/PathBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.builder.route.RouteMeta 4 | import io.ktor.http.HttpMethod 5 | import io.swagger.v3.oas.models.PathItem 6 | 7 | /** 8 | * Build the openapi [PathItem]-object. Holds information describing the operations available on a single path. 9 | * See [OpenAPI Specification - Path Item Object](https://swagger.io/specification/#path-item-object). 10 | */ 11 | internal class PathBuilder( 12 | private val operationBuilder: OperationBuilder 13 | ) { 14 | 15 | fun build(route: RouteMeta): PathItem = 16 | PathItem().also { 17 | val operation = operationBuilder.build(route) 18 | when (route.method) { 19 | HttpMethod.Get -> it.get = operation 20 | HttpMethod.Post -> it.post = operation 21 | HttpMethod.Put -> it.put = operation 22 | HttpMethod.Patch -> it.patch = operation 23 | HttpMethod.Delete -> it.delete = operation 24 | HttpMethod.Head -> it.head = operation 25 | HttpMethod.Options -> it.options = operation 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/PathsBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.builder.route.RouteMeta 4 | import io.github.smiley4.ktoropenapi.data.OpenApiPluginData 5 | import io.swagger.v3.oas.models.PathItem 6 | import io.swagger.v3.oas.models.Paths 7 | 8 | /** 9 | * Build the openapi [Paths]-object. Holds the relative paths to the individual endpoints and their operations. 10 | * See [OpenAPI Specification - Paths Object](https://swagger.io/specification/#paths-object). 11 | */ 12 | internal class PathsBuilder( 13 | private val config: OpenApiPluginData, 14 | private val pathBuilder: PathBuilder 15 | ) { 16 | 17 | fun build(routes: Collection): Paths = 18 | Paths().also { 19 | routes.forEach { route -> 20 | val url = createUrl(route) 21 | val existingPath = it[url] 22 | if (existingPath != null) { 23 | addToExistingPath(existingPath, route) 24 | } else { 25 | addAsNewPath(url, it, route) 26 | } 27 | } 28 | } 29 | 30 | private fun createUrl(route: RouteMeta): String { 31 | return "${config.rootPath ?: ""}${route.path}" 32 | } 33 | 34 | private fun addAsNewPath(url: String, paths: Paths, route: RouteMeta) { 35 | paths.addPathItem(url, pathBuilder.build(route)) 36 | } 37 | 38 | private fun addToExistingPath(existing: PathItem, route: RouteMeta) { 39 | val path = pathBuilder.build(route) 40 | existing.get = path.get ?: existing.get 41 | existing.put = path.put ?: existing.put 42 | existing.post = path.post ?: existing.post 43 | existing.delete = path.delete ?: existing.delete 44 | existing.options = path.options ?: existing.options 45 | existing.head = path.head ?: existing.head 46 | existing.patch = path.patch ?: existing.patch 47 | existing.trace = path.trace ?: existing.trace 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/RequestBodyBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.BaseBodyData 4 | import io.swagger.v3.oas.models.parameters.RequestBody 5 | 6 | /** 7 | * Build the openapi [RequestBody]-object. Holds information describing a single request body. 8 | * See [OpenAPI Specification - Request Body Object](https://swagger.io/specification/#request-body-object). 9 | */ 10 | internal class RequestBodyBuilder( 11 | private val contentBuilder: ContentBuilder 12 | ) { 13 | 14 | fun build(body: BaseBodyData): RequestBody = 15 | RequestBody().also { 16 | it.description = body.description 17 | it.required = body.required 18 | it.content = contentBuilder.build(body) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/ResponseBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.ResponseData 4 | import io.swagger.v3.oas.models.responses.ApiResponse 5 | 6 | /** 7 | * Build the openapi [ApiResponse]-objects by status-code. Holds information describing status-codes and responses from an API Operation. 8 | * See [OpenAPI Specification - Response Object](https://swagger.io/specification/#response-object). 9 | */ 10 | internal class ResponseBuilder( 11 | private val headerBuilder: HeaderBuilder, 12 | private val contentBuilder: ContentBuilder 13 | ) { 14 | 15 | fun build(response: ResponseData): Pair = 16 | response.statusCode to ApiResponse().also { 17 | it.description = response.description 18 | it.headers = response.headers.mapValues { header -> headerBuilder.build(header.value) } 19 | response.body?.let { body -> 20 | it.content = contentBuilder.build(body) 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/ResponsesBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.ResponseData 4 | import io.github.smiley4.ktoropenapi.data.OpenApiPluginData 5 | import io.ktor.http.HttpStatusCode 6 | import io.swagger.v3.oas.models.responses.ApiResponses 7 | 8 | /** 9 | * Build the openapi [ApiResponses]-object. A container for the expected responses of an operation. 10 | * See [OpenAPI Specification - Responses Object](https://swagger.io/specification/#responses-object). 11 | */ 12 | internal class ResponsesBuilder( 13 | private val responseBuilder: ResponseBuilder, 14 | private val config: OpenApiPluginData 15 | ) { 16 | 17 | fun build(responses: List, isProtected: Boolean): ApiResponses = 18 | ApiResponses().also { 19 | responses 20 | .filter { !it.hidden } 21 | .map { response -> responseBuilder.build(response) } 22 | .forEach { (name, response) -> it.addApiResponse(name, response) } 23 | if (shouldAddUnauthorized(responses, isProtected)) { 24 | config.securityConfig.defaultUnauthorizedResponse 25 | ?.let { response -> responseBuilder.build(response) } 26 | ?.also { (name, response) -> it.addApiResponse(name, response) } 27 | } 28 | } 29 | 30 | private fun shouldAddUnauthorized(responses: List, isProtected: Boolean): Boolean { 31 | val unauthorizedCode = HttpStatusCode.Unauthorized.value.toString() 32 | return config.securityConfig.defaultUnauthorizedResponse != null 33 | && !config.securityConfig.defaultUnauthorizedResponse.hidden 34 | && isProtected 35 | && responses.count { it.statusCode == unauthorizedCode } == 0 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/SecurityRequirementsBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.builder.route.RouteMeta 4 | import io.github.smiley4.ktoropenapi.data.OpenApiPluginData 5 | import io.swagger.v3.oas.models.security.SecurityRequirement 6 | 7 | /** 8 | * Build the openapi [SecurityRequirement]-objects. 9 | * See [OpenAPI Specification - Security Requirement Object](https://swagger.io/specification/#security-requirement-object). 10 | */ 11 | internal class SecurityRequirementsBuilder( 12 | private val config: OpenApiPluginData 13 | ) { 14 | 15 | fun build(route: RouteMeta): List { 16 | return buildSet { 17 | addAll(route.documentation.securitySchemeNames) 18 | if(route.documentation.securitySchemeNames.isEmpty()) { 19 | addAll(config.securityConfig.defaultSecuritySchemeNames) 20 | } 21 | }.map { 22 | SecurityRequirement().apply { 23 | addList(it, emptyList()) 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/SecuritySchemesBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.config.AuthType 4 | import io.github.smiley4.ktoropenapi.data.SecuritySchemeData 5 | import io.swagger.v3.oas.models.security.SecurityScheme 6 | 7 | /** 8 | * Build the openapi [SecurityScheme]-objects with their names. Holds information defining security schemes that can be used by operations. 9 | * See [OpenAPI Specification - Security Scheme Object](https://swagger.io/specification/#security-scheme-object). 10 | */ 11 | internal class SecuritySchemesBuilder( 12 | private val oAuthFlowsBuilder: OAuthFlowsBuilder 13 | ) { 14 | 15 | fun build(securitySchemes: List): Map { 16 | return mutableMapOf().apply { 17 | securitySchemes.forEach { 18 | put(it.schemeName, build(it)) 19 | } 20 | } 21 | } 22 | 23 | private fun build(securityScheme: SecuritySchemeData): SecurityScheme { 24 | return SecurityScheme().apply { 25 | description = securityScheme.description 26 | name = if(securityScheme.type == AuthType.API_KEY) securityScheme.name ?: securityScheme.schemeName else null 27 | type = securityScheme.type?.swaggerType 28 | `in` = securityScheme.location?.swaggerType 29 | scheme = securityScheme.scheme?.swaggerType 30 | bearerFormat = securityScheme.bearerFormat 31 | flows = securityScheme.flows?.let { f -> oAuthFlowsBuilder.build(f) } 32 | openIdConnectUrl = securityScheme.openIdConnectUrl 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/ServerBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.ServerData 4 | import io.swagger.v3.oas.models.servers.Server 5 | import io.swagger.v3.oas.models.servers.ServerVariable 6 | import io.swagger.v3.oas.models.servers.ServerVariables 7 | 8 | /** 9 | * Build the openapi [Server]-object. Holds information representing a Server. 10 | * See [OpenAPI Specification - Server Object](https://swagger.io/specification/#server-object). 11 | */ 12 | internal class ServerBuilder { 13 | 14 | fun build(server: ServerData): Server = 15 | Server().also { 16 | it.url = server.url 17 | it.description = server.description 18 | if (server.variables.isNotEmpty()) { 19 | it.variables = ServerVariables().also { variables -> 20 | server.variables.forEach { entry -> 21 | variables.addServerVariable(entry.name, ServerVariable().also { variable -> 22 | variable.enum = entry.enum.toList() 23 | variable.default = entry.default 24 | variable.description = entry.description 25 | }) 26 | } 27 | } 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/TagBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.data.TagData 4 | import io.swagger.v3.oas.models.tags.Tag 5 | 6 | /** 7 | * Build the openapi [Tag]-object. Holds metadata of a single tag. 8 | * See [OpenAPI Specification - Tag Object](https://swagger.io/specification/#tag-object). 9 | */ 10 | internal class TagBuilder( 11 | private val tagExternalDocumentationBuilder: TagExternalDocumentationBuilder 12 | ) { 13 | 14 | fun build(tag: TagData): Tag = 15 | Tag().also { 16 | it.name = tag.name 17 | it.description = tag.description 18 | if(tag.externalDocUrl != null && tag.externalDocDescription != null) { 19 | it.externalDocs = tagExternalDocumentationBuilder.build(tag.externalDocUrl, tag.externalDocDescription) 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/TagExternalDocumentationBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.swagger.v3.oas.models.ExternalDocumentation 4 | 5 | /** 6 | * Build the openapi [ExternalDocumentation]-object for a tag. 7 | * See [OpenAPI Specification - External Documentation Object](https://swagger.io/specification/#external-documentation-object). 8 | */ 9 | internal class TagExternalDocumentationBuilder { 10 | 11 | fun build(url: String, description: String): ExternalDocumentation = 12 | ExternalDocumentation().also { 13 | it.url = url 14 | it.description = description 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/WebhooksBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.openapi 2 | 3 | import io.github.smiley4.ktoropenapi.builder.route.RouteMeta 4 | import io.swagger.v3.oas.models.PathItem 5 | 6 | /** 7 | * Build the openapi "webhooks" section. 8 | * See [OpenAPI Specification - Webhooks](https://spec.openapis.org/oas/v3.1.0.html#oasWebhooks) 9 | */ 10 | internal class WebhooksBuilder( 11 | private val pathBuilder: PathBuilder 12 | ) { 13 | 14 | fun build(routes: Collection): Map { 15 | return routes.associate { route -> 16 | route.path to pathBuilder.build(route) 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteDocumentationMerger.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.route 2 | 3 | import io.github.smiley4.ktoropenapi.config.RouteConfig 4 | 5 | internal class RouteDocumentationMerger { 6 | 7 | /** 8 | * Merges "a" with "b" and returns the result as a new [RouteConfig]. "a" has priority over "b". 9 | */ 10 | @Suppress("CyclomaticComplexMethod") 11 | fun merge(a: RouteConfig, b: RouteConfig): RouteConfig { 12 | return RouteConfig().apply { 13 | specName = a.specName ?: b.specName 14 | tags = mutableSetOf().also { 15 | it.addAll(a.tags) 16 | it.addAll(b.tags) 17 | } 18 | summary = a.summary ?: b.summary 19 | description = a.description ?: b.description 20 | operationId = a.operationId ?: b.operationId 21 | securitySchemeNames = mutableSetOf().also { merged -> 22 | a.securitySchemeNames?.let { merged.addAll(it) } 23 | b.securitySchemeNames?.let { merged.addAll(it) } 24 | } 25 | deprecated = a.deprecated || b.deprecated 26 | hidden = a.hidden || b.hidden 27 | protected = a.protected ?: b.protected 28 | request { 29 | buildMap { 30 | b.getRequest().parameters.forEach { this[it.name] = it } 31 | a.getRequest().parameters.forEach { this[it.name] = it } 32 | }.values.forEach { parameters.add(it) } 33 | setBody(a.getRequest().getBody() ?: b.getRequest().getBody()) 34 | } 35 | response { 36 | buildMap { 37 | b.getResponses().getResponses().forEach { this[it.statusCode] = it } 38 | a.getResponses().getResponses().forEach { this[it.statusCode] = it } 39 | }.values.forEach { addResponse(it) } 40 | } 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteMeta.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.route 2 | 3 | import io.github.smiley4.ktoropenapi.data.RouteData 4 | import io.ktor.http.HttpMethod 5 | 6 | /** 7 | * Information about a route 8 | */ 9 | internal data class RouteMeta( 10 | val path: String, 11 | val method: HttpMethod, 12 | val documentation: RouteData, 13 | val protected: Boolean, 14 | val isWebhook: Boolean, 15 | ) 16 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/schema/SchemaContext.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.builder.schema 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 4 | import io.swagger.v3.oas.models.media.Schema 5 | 6 | /** 7 | * Provides schemas for an openapi-spec 8 | */ 9 | internal interface SchemaContext { 10 | 11 | /** 12 | * Get a [Schema] (or a ref to a schema) by its [TypeDescriptor] 13 | */ 14 | fun getSchema(typeDescriptor: TypeDescriptor): Schema<*> 15 | 16 | /** 17 | * Get all schemas placed in the components-section of the spec. 18 | */ 19 | fun getComponentSection(): Map> 20 | } 21 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/AuthKeyLocation.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.swagger.v3.oas.models.security.SecurityScheme 4 | 5 | /** 6 | * The locations of the API key. 7 | */ 8 | enum class AuthKeyLocation(val swaggerType: SecurityScheme.In) { 9 | QUERY(SecurityScheme.In.QUERY), 10 | HEADER(SecurityScheme.In.HEADER), 11 | COOKIE(SecurityScheme.In.COOKIE) 12 | } 13 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/AuthScheme.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | /** 4 | * The authentication scheme. 5 | * https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml 6 | */ 7 | enum class AuthScheme(val swaggerType: String) { 8 | BASIC("Basic"), 9 | BEARER("bearer"), 10 | DIGEST("Digest"), 11 | HOBA("HOBA"), 12 | MUTUAL("Mutual"), 13 | OAUTH("OAuth"), 14 | SCRAM_SHA_1("SCRAM-SHA-1"), 15 | SCRAM_SHA_256("SCRAM-SHA-256"), 16 | VAPID("vapid") 17 | } 18 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/AuthType.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.swagger.v3.oas.models.security.SecurityScheme 4 | 5 | /** 6 | * The type of security schemes 7 | */ 8 | enum class AuthType(val swaggerType: SecurityScheme.Type) { 9 | API_KEY(SecurityScheme.Type.APIKEY), 10 | HTTP(SecurityScheme.Type.HTTP), 11 | OAUTH2(SecurityScheme.Type.OAUTH2), 12 | OPENID_CONNECT(SecurityScheme.Type.OPENIDCONNECT), 13 | MUTUAL_TLS(SecurityScheme.Type.MUTUALTLS) 14 | } 15 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/BaseBodyConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.BaseBodyData 4 | import io.ktor.http.ContentType 5 | 6 | /** 7 | * Describes a single request/response body with a single schema. 8 | */ 9 | @OpenApiDslMarker 10 | sealed class BaseBodyConfig { 11 | 12 | /** 13 | * A brief description of the request body 14 | */ 15 | var description: String? = null 16 | 17 | /** 18 | * Determines if the request body is required in the request 19 | */ 20 | var required: Boolean? = null 21 | 22 | /** 23 | * Allowed Media Types for this body. If none specified, a media type will be chosen automatically based on the provided schema 24 | */ 25 | var mediaTypes: Collection = emptySet() 26 | 27 | /** 28 | * Set the allowed Media Types for this body. If none specified, a media type will be chosen automatically based on the provided schema 29 | */ 30 | fun mediaTypes(mediaTypes: Collection) { 31 | this.mediaTypes = mediaTypes 32 | } 33 | 34 | /** 35 | * Set the allowed Media Types for this body. If none specified, a media type will be chosen automatically based on the provided schema 36 | */ 37 | fun mediaTypes(vararg mediaTypes: ContentType) { 38 | this.mediaTypes = mediaTypes.toList() 39 | } 40 | 41 | /** 42 | * Build the data object for this config. 43 | */ 44 | internal abstract fun build(): BaseBodyData 45 | } 46 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ContactConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.ContactData 4 | import io.github.smiley4.ktoropenapi.data.DataUtils.merge 5 | 6 | /** 7 | * Contact information for the exposed API. 8 | */ 9 | @OpenApiDslMarker 10 | class ContactConfig internal constructor() { 11 | 12 | /** 13 | * The identifying name of the contact person/organization. 14 | */ 15 | var name: String? = ContactData.DEFAULT.name 16 | 17 | 18 | /** 19 | * The URL pointing to the contact information. MUST be in the format of a URL. 20 | */ 21 | var url: String? = ContactData.DEFAULT.url 22 | 23 | 24 | /** 25 | * The email address of the contact person/organization. MUST be in the format of an email address. 26 | */ 27 | var email: String? = ContactData.DEFAULT.email 28 | 29 | 30 | /** 31 | * Build the data object for this config. 32 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 33 | */ 34 | internal fun build(base: ContactData) = ContactData( 35 | name = merge(base.name, name), 36 | url = merge(base.url, url), 37 | email = merge(base.email, email) 38 | ) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ExampleConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.ExampleDescriptor 4 | import io.github.smiley4.ktoropenapi.config.descriptors.SwaggerExampleDescriptor 5 | import io.github.smiley4.ktoropenapi.config.descriptors.ValueExampleDescriptor 6 | import io.github.smiley4.ktoropenapi.data.ExampleConfigData 7 | import io.github.smiley4.ktoropenapi.data.MultipartBodyData 8 | import io.github.smiley4.ktoropenapi.data.SecurityData 9 | import io.github.smiley4.ktoropenapi.data.SimpleBodyData 10 | import io.swagger.v3.oas.models.examples.Example 11 | 12 | 13 | /** 14 | * Configuration for examples 15 | */ 16 | @OpenApiDslMarker 17 | class ExampleConfig internal constructor() { 18 | 19 | /** 20 | * The list of global / shared examples. 21 | */ 22 | private val sharedExamples = mutableMapOf() 23 | 24 | 25 | /** 26 | * Add a shared example that can be referenced by all routes. 27 | * The name of the example has to be unique among all shared examples and acts as its id. 28 | * @param example the example data. 29 | */ 30 | fun example(example: ExampleDescriptor) { 31 | sharedExamples[example.name] = example 32 | } 33 | 34 | 35 | /** 36 | * Add a shared example that can be referenced by all routes by the given name. 37 | * The provided name has to be unique among all shared examples and acts as its id. 38 | */ 39 | fun example(name: String, example: Example) = example(SwaggerExampleDescriptor(name, example)) 40 | 41 | 42 | /** 43 | * Add a shared example that can be referenced by all routes by the given name. 44 | * The provided name has to be unique among all shared examples and acts as its id. 45 | */ 46 | fun example(name: String, example: ValueExampleDescriptorConfig.() -> Unit) = example( 47 | ValueExampleDescriptorConfig() 48 | .apply(example) 49 | .let { result -> 50 | ValueExampleDescriptor( 51 | name = name, 52 | value = result.value, 53 | summary = result.summary, 54 | description = result.description 55 | ) 56 | } 57 | ) 58 | 59 | 60 | /** 61 | * The [GenericExampleEncoder] responsible for encoding all example values. 62 | */ 63 | var exampleEncoder: GenericExampleEncoder = ExampleConfigData.DEFAULT.exampleEncoder 64 | 65 | 66 | /** 67 | * Specify a custom encoder for example objects 68 | */ 69 | fun encoder(exampleEncoder: GenericExampleEncoder) { 70 | this.exampleEncoder = exampleEncoder 71 | } 72 | 73 | 74 | /** 75 | * Build the data object for this config. 76 | * @param securityConfig the data for security config that might contain additional examples 77 | */ 78 | internal fun build(securityConfig: SecurityData) = ExampleConfigData( 79 | sharedExamples = sharedExamples, 80 | securityExamples = securityConfig.defaultUnauthorizedResponse?.body?.let { 81 | when (it) { 82 | is SimpleBodyData -> it 83 | is MultipartBodyData -> null 84 | } 85 | }, 86 | exampleEncoder = exampleEncoder 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ExampleEncoder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference 4 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 5 | import io.github.smiley4.ktoropenapi.config.descriptors.KTypeDescriptor 6 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 7 | import kotlinx.serialization.json.Json 8 | import kotlinx.serialization.serializer 9 | 10 | /** 11 | * Encoder to produce the final example value. 12 | * Return the unmodified example to fall back to the default encoder. 13 | */ 14 | typealias GenericExampleEncoder = (type: TypeDescriptor?, example: Any?) -> Any? 15 | 16 | 17 | object ExampleEncoder { 18 | 19 | /** 20 | * Default [GenericExampleEncoder] using internal swagger serializer to encode example object. 21 | */ 22 | fun internal(): GenericExampleEncoder = { _, example -> 23 | example 24 | } 25 | 26 | /** 27 | * [GenericExampleEncoder] using kotlinx-serialization to encode example objects. 28 | * @param json the kotlinx json serializer to use for encoding objects to json. Set `null` to use default kotlinx json serializer. 29 | */ 30 | fun kotlinx(json: Json? = null): GenericExampleEncoder = { type, example -> 31 | when(type) { 32 | is KTypeDescriptor -> { 33 | val jsonEncoder = json ?: Json 34 | val jsonString = jsonEncoder.encodeToString(serializer(type.type), example) 35 | val jsonObj = jacksonObjectMapper().readValue(jsonString, object : TypeReference() {}) 36 | jsonObj 37 | } 38 | else -> example 39 | } 40 | } 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ExternalDocsConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.DataUtils 4 | import io.github.smiley4.ktoropenapi.data.ExternalDocsData 5 | 6 | /** 7 | * An object representing external documentation. 8 | */ 9 | @OpenApiDslMarker 10 | class ExternalDocsConfig internal constructor() { 11 | 12 | /** 13 | * A short description of the external documentation 14 | */ 15 | var description: String? = null 16 | 17 | 18 | /** 19 | * A URL to the external documentation 20 | */ 21 | var url: String = "/" 22 | 23 | /** 24 | * Build the data object for this config. 25 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 26 | */ 27 | internal fun build(base: ExternalDocsData) = ExternalDocsData( 28 | url = DataUtils.mergeDefault(base.url, url, ExternalDocsData.DEFAULT.url), 29 | description = DataUtils.merge(base.description, description) 30 | ) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/HeaderConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.KTypeDescriptor 4 | import io.github.smiley4.ktoropenapi.config.descriptors.SwaggerTypeDescriptor 5 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 6 | import io.github.smiley4.ktoropenapi.data.HeaderData 7 | import io.swagger.v3.oas.models.media.Schema 8 | import kotlin.reflect.KType 9 | import kotlin.reflect.typeOf 10 | 11 | /** 12 | * Describes a single header. 13 | */ 14 | @OpenApiDslMarker 15 | class HeaderConfig internal constructor() { 16 | 17 | /** 18 | * A description of the header 19 | */ 20 | var description: String? = null 21 | 22 | 23 | /** 24 | * The schema of the header 25 | */ 26 | internal var type: TypeDescriptor? = null 27 | 28 | 29 | /** 30 | * The schema of the header 31 | */ 32 | fun type(type: TypeDescriptor) { 33 | this.type = type 34 | } 35 | 36 | 37 | /** 38 | * The schema of the header 39 | */ 40 | fun type(type: Schema<*>) = type(SwaggerTypeDescriptor(type)) 41 | 42 | 43 | /** 44 | * The schema of the header 45 | */ 46 | fun type(type: KType) = type(KTypeDescriptor(type)) 47 | 48 | 49 | /** 50 | * The schema of the header 51 | */ 52 | inline fun type() = type(KTypeDescriptor(typeOf())) 53 | 54 | 55 | /** 56 | * Determines whether this header is mandatory 57 | */ 58 | var required: Boolean? = null 59 | 60 | 61 | /** 62 | * Specifies that a header is deprecated and SHOULD be transitioned out of usage 63 | */ 64 | var deprecated: Boolean? = null 65 | 66 | 67 | /** 68 | * Specifies whether arrays and objects should generate separate parameters for each array item or object property. 69 | */ 70 | var explode: Boolean? = null 71 | 72 | /** 73 | * Build the data object for this config. 74 | */ 75 | internal fun build() = HeaderData( 76 | description = description, 77 | type = type, 78 | required = required ?: false, 79 | deprecated = deprecated ?: false, 80 | explode = explode, 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/InfoConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.ContactData 4 | import io.github.smiley4.ktoropenapi.data.DataUtils.merge 5 | import io.github.smiley4.ktoropenapi.data.DataUtils.mergeDefault 6 | import io.github.smiley4.ktoropenapi.data.InfoData 7 | import io.github.smiley4.ktoropenapi.data.LicenseData 8 | 9 | /** 10 | * Basic information for the exposed API. 11 | */ 12 | @OpenApiDslMarker 13 | class InfoConfig internal constructor() { 14 | 15 | /** 16 | * The title of the api 17 | */ 18 | var title: String = "API" 19 | 20 | 21 | /** 22 | * The version of the OpenAPI document 23 | */ 24 | var version: String? = "latest" 25 | 26 | 27 | /** 28 | * A short description of the API 29 | */ 30 | var description: String? = null 31 | 32 | /** 33 | * A short summary of the API 34 | */ 35 | var summary: String? = null 36 | 37 | 38 | /** 39 | * A URL to the Terms of Service for the API. MUST be in the format of a URL. 40 | */ 41 | var termsOfService: String? = null 42 | 43 | private var contact: ContactConfig? = null 44 | 45 | 46 | /** 47 | * The contact information for the exposed API. 48 | */ 49 | fun contact(block: ContactConfig.() -> Unit) { 50 | contact = ContactConfig().apply(block) 51 | } 52 | 53 | 54 | private var license: LicenseConfig? = null 55 | 56 | 57 | /** 58 | * The license information for the exposed API. 59 | */ 60 | fun license(block: LicenseConfig.() -> Unit) { 61 | license = LicenseConfig().apply(block) 62 | } 63 | 64 | /** 65 | * Build the data object for this config. 66 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 67 | */ 68 | internal fun build(base: InfoData): InfoData { 69 | return InfoData( 70 | title = mergeDefault(base.title, this.title, InfoData.DEFAULT.title), 71 | version = merge(base.version, this.version), 72 | description = merge(base.description, this.description), 73 | termsOfService = merge(base.termsOfService, this.termsOfService), 74 | contact = contact?.build(base.contact ?: ContactData.DEFAULT) ?: base.contact, 75 | license = license?.build(base.license ?: LicenseData.DEFAULT) ?: base.license, 76 | summary = merge(base.summary, this.summary) 77 | ) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/LicenseConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.DataUtils 4 | import io.github.smiley4.ktoropenapi.data.LicenseData 5 | 6 | /** 7 | * License information for the exposed API. 8 | */ 9 | @OpenApiDslMarker 10 | class LicenseConfig internal constructor() { 11 | 12 | /** 13 | * The license name used for the API 14 | */ 15 | var name: String? = LicenseData.DEFAULT.name 16 | 17 | 18 | /** 19 | * A URL to the license used for the API. MUST be in the format of a URL. 20 | */ 21 | var url: String? = LicenseData.DEFAULT.url 22 | 23 | 24 | /** 25 | * An SPDX (https://spdx.org/licenses/) license expression for the API. The identifier field is mutually exclusive of the url field. 26 | */ 27 | var identifier: String? = LicenseData.DEFAULT.identifier 28 | 29 | /** 30 | * Build the data object for this config. 31 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 32 | */ 33 | internal fun build(base: LicenseData) = LicenseData( 34 | name = DataUtils.merge(base.name, name), 35 | url = DataUtils.merge(base.url, url), 36 | identifier = DataUtils.merge(base.identifier, identifier) 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/MultipartBodyConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.KTypeDescriptor 4 | import io.github.smiley4.ktoropenapi.config.descriptors.SwaggerTypeDescriptor 5 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 6 | import io.github.smiley4.ktoropenapi.data.MultipartBodyData 7 | import io.swagger.v3.oas.models.media.Schema 8 | import kotlin.reflect.KType 9 | import kotlin.reflect.typeOf 10 | 11 | 12 | /** 13 | * Describes a single request/response body with multipart content. 14 | * See https://swagger.io/docs/specification/describing-request-body/multipart-requests/ for more info 15 | */ 16 | @OpenApiDslMarker 17 | class MultipartBodyConfig : BaseBodyConfig() { 18 | 19 | private val parts = mutableListOf() 20 | 21 | 22 | /** 23 | * One part of a multipart-body 24 | */ 25 | fun part(name: String, type: TypeDescriptor, block: MultipartPartConfig.() -> Unit = {}) { 26 | parts.add(MultipartPartConfig(name, type).apply(block)) 27 | } 28 | 29 | 30 | /** 31 | * One part of a multipart-body 32 | */ 33 | fun part(name: String, type: Schema<*>, block: MultipartPartConfig.() -> Unit = {}) = part(name, SwaggerTypeDescriptor(type), block) 34 | 35 | 36 | /** 37 | * One part of a multipart-body 38 | */ 39 | fun part(name: String, type: KType, block: MultipartPartConfig.() -> Unit = {}) = part(name, KTypeDescriptor(type), block) 40 | 41 | 42 | /** 43 | * One part of a multipart-body 44 | */ 45 | inline fun part(name: String, noinline block: MultipartPartConfig.() -> Unit = {}) = 46 | part(name, KTypeDescriptor(typeOf()), block) 47 | 48 | 49 | override fun build() = MultipartBodyData( 50 | description = description, 51 | required = required ?: false, 52 | mediaTypes = mediaTypes.toSet(), 53 | parts = parts.map { it.build() } 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/MultipartPartConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.KTypeDescriptor 4 | import io.github.smiley4.ktoropenapi.config.descriptors.SwaggerTypeDescriptor 5 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 6 | import io.github.smiley4.ktoropenapi.data.MultipartPartData 7 | import io.ktor.http.ContentType 8 | import io.swagger.v3.oas.models.media.Schema 9 | import kotlin.reflect.KType 10 | import kotlin.reflect.typeOf 11 | 12 | /** 13 | * Describes one section of a multipart-body. 14 | * See https://swagger.io/docs/specification/describing-request-body/multipart-requests/ for more info 15 | */ 16 | @OpenApiDslMarker 17 | class MultipartPartConfig internal constructor( 18 | /** 19 | * The name of this part 20 | */ 21 | val name: String, 22 | 23 | val type: TypeDescriptor 24 | ) { 25 | 26 | /** 27 | * Whether this part is required 28 | */ 29 | var required: Boolean = false 30 | 31 | /** 32 | * Specific content types for this part 33 | */ 34 | var mediaTypes: Collection = setOf() 35 | 36 | /** 37 | * Set specific content types for this part 38 | */ 39 | fun mediaTypes(types: Collection) { 40 | this.mediaTypes = types 41 | } 42 | 43 | /** 44 | * Set specific content types for this part 45 | */ 46 | fun mediaTypes(vararg types: ContentType) { 47 | this.mediaTypes = types.toList() 48 | } 49 | 50 | /** 51 | * List of headers of this part 52 | */ 53 | val headers = mutableMapOf() 54 | 55 | 56 | /** 57 | * Possible headers for this part 58 | */ 59 | fun header(name: String, type: TypeDescriptor, block: HeaderConfig.() -> Unit = {}) { 60 | headers[name] = HeaderConfig().apply(block).apply { 61 | this.type = type 62 | } 63 | } 64 | 65 | 66 | /** 67 | * Possible headers for this part 68 | */ 69 | fun header(name: String, type: Schema<*>, block: HeaderConfig.() -> Unit = {}) = header(name, SwaggerTypeDescriptor(type), block) 70 | 71 | 72 | /** 73 | * Possible headers for this part 74 | */ 75 | fun header(name: String, type: KType, block: HeaderConfig.() -> Unit = {}) = header(name, KTypeDescriptor(type), block) 76 | 77 | 78 | /** 79 | * Possible headers for this part 80 | */ 81 | inline fun header(name: String, noinline block: HeaderConfig.() -> Unit = {}) = 82 | header(name, KTypeDescriptor(typeOf()), block) 83 | 84 | /** 85 | * Build the data object for this config. 86 | */ 87 | internal fun build() = MultipartPartData( 88 | name = name, 89 | type = type, 90 | required = required, 91 | mediaTypes = mediaTypes.toSet(), 92 | headers = headers.mapValues { it.value.build() } 93 | ) 94 | 95 | } 96 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/OpenApiDslMarker.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | @DslMarker 4 | annotation class OpenApiDslMarker 5 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/OpenIdOAuthFlowConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.DataUtils.merge 4 | import io.github.smiley4.ktoropenapi.data.OpenIdOAuthFlowData 5 | 6 | /** 7 | * Configuration details for a supported OAuth Flow 8 | */ 9 | @OpenApiDslMarker 10 | class OpenIdOAuthFlowConfig internal constructor() { 11 | 12 | /** 13 | * The authorization URL to be used for this flow 14 | */ 15 | var authorizationUrl: String? = null 16 | 17 | /** 18 | * The token URL to be used for this flow 19 | */ 20 | var tokenUrl: String? = null 21 | 22 | /** 23 | * The URL to be used for obtaining refresh tokens 24 | */ 25 | var refreshUrl: String? = null 26 | 27 | /** 28 | * The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it 29 | */ 30 | var scopes: Map? = null 31 | 32 | /** 33 | * Build the data object for this config. 34 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 35 | */ 36 | internal fun build(base: OpenIdOAuthFlowData) = OpenIdOAuthFlowData( 37 | authorizationUrl = merge(base.authorizationUrl, authorizationUrl), 38 | tokenUrl = merge(base.tokenUrl, tokenUrl), 39 | refreshUrl = merge(base.refreshUrl, refreshUrl), 40 | scopes = merge(base.scopes, scopes), 41 | ) 42 | 43 | } 44 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/OpenIdOAuthFlowsConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.OpenIdOAuthFlowData 4 | import io.github.smiley4.ktoropenapi.data.OpenIdOAuthFlowsData 5 | 6 | /** 7 | * An object containing configuration information for the oauth flow types supported 8 | */ 9 | @OpenApiDslMarker 10 | class OpenIdOAuthFlowsConfig internal constructor() { 11 | 12 | private var implicit: OpenIdOAuthFlowConfig? = null 13 | 14 | 15 | /** 16 | * Configuration for the OAuth Implicit flow 17 | */ 18 | fun implicit(block: OpenIdOAuthFlowConfig.() -> Unit) { 19 | implicit = OpenIdOAuthFlowConfig().apply(block) 20 | } 21 | 22 | 23 | private var password: OpenIdOAuthFlowConfig? = null 24 | 25 | 26 | /** 27 | * Configuration for the OAuth Resource Owner Password flow 28 | */ 29 | fun password(block: OpenIdOAuthFlowConfig.() -> Unit) { 30 | password = OpenIdOAuthFlowConfig().apply(block) 31 | } 32 | 33 | 34 | private var clientCredentials: OpenIdOAuthFlowConfig? = null 35 | 36 | 37 | /** 38 | * Configuration for the OAuth Client Credentials flow. 39 | */ 40 | fun clientCredentials(block: OpenIdOAuthFlowConfig.() -> Unit) { 41 | clientCredentials = OpenIdOAuthFlowConfig().apply(block) 42 | } 43 | 44 | 45 | private var authorizationCode: OpenIdOAuthFlowConfig? = null 46 | 47 | 48 | /** 49 | * Configuration for the OAuth Authorization Code flow. 50 | */ 51 | fun authorizationCode(block: OpenIdOAuthFlowConfig.() -> Unit) { 52 | authorizationCode = OpenIdOAuthFlowConfig().apply(block) 53 | } 54 | 55 | /** 56 | * Build the data object for this config. 57 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 58 | */ 59 | internal fun build(base: OpenIdOAuthFlowsData) = OpenIdOAuthFlowsData( 60 | implicit = implicit?.build(base.implicit ?: OpenIdOAuthFlowData.DEFAULT) ?: base.implicit, 61 | password = password?.build(base.password ?: OpenIdOAuthFlowData.DEFAULT) ?: base.password, 62 | clientCredentials = clientCredentials?.build(base.clientCredentials ?: OpenIdOAuthFlowData.DEFAULT) ?: base.clientCredentials, 63 | authorizationCode = authorizationCode?.build(base.authorizationCode ?: OpenIdOAuthFlowData.DEFAULT) ?: base.authorizationCode, 64 | ) 65 | 66 | } 67 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/OutputFormat.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | enum class OutputFormat(val empty: String) { 4 | JSON("{}"), 5 | YAML("") 6 | } 7 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ParameterLocation.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | /** 4 | * Locations for request parameters. 5 | */ 6 | enum class ParameterLocation { 7 | QUERY, HEADER, PATH, COOKIE 8 | } 9 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/PathFilter.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.ktor.http.HttpMethod 4 | 5 | /** 6 | * Filters paths to determine which to include (return 'true') in the spec and which to hide (return 'true'). 7 | */ 8 | typealias PathFilter = (method: HttpMethod, url: List) -> Boolean 9 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/PostBuild.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.swagger.v3.oas.models.OpenAPI 4 | 5 | /** 6 | * Function executed after building the openapi-spec. 7 | */ 8 | typealias PostBuild = (openApi: OpenAPI, specName: String) -> Unit 9 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/RequestParameterConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.ExampleDescriptor 4 | import io.github.smiley4.ktoropenapi.config.descriptors.SwaggerExampleDescriptor 5 | import io.github.smiley4.ktoropenapi.config.descriptors.ValueExampleDescriptor 6 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 7 | import io.github.smiley4.ktoropenapi.data.RequestParameterData 8 | import io.swagger.v3.oas.models.examples.Example 9 | import io.swagger.v3.oas.models.parameters.Parameter 10 | 11 | /** 12 | * Describes a single request parameter. 13 | */ 14 | @OpenApiDslMarker 15 | class RequestParameterConfig internal constructor( 16 | /** 17 | * The name (case-sensitive) of the parameter 18 | */ 19 | val name: String, 20 | /** 21 | * The type defining the schema used for the parameter. 22 | */ 23 | val type: TypeDescriptor, 24 | /** 25 | * Location of the parameter 26 | */ 27 | val location: ParameterLocation 28 | ) { 29 | 30 | /** 31 | * A brief description of the parameter 32 | */ 33 | var description: String? = null 34 | 35 | 36 | /** 37 | * An example value for this parameter 38 | */ 39 | internal var example: ExampleDescriptor? = null 40 | 41 | 42 | /** 43 | * An example value for this parameter 44 | */ 45 | fun example(example: ExampleDescriptor) { 46 | this.example = example 47 | } 48 | 49 | 50 | /** 51 | * An example value for this parameter 52 | */ 53 | fun example(name: String, example: Example) = example(SwaggerExampleDescriptor(name, example)) 54 | 55 | 56 | /** 57 | * An example value for this parameter 58 | */ 59 | fun example(name: String, example: ValueExampleDescriptorConfig.() -> Unit) = example( 60 | ValueExampleDescriptorConfig() 61 | .apply(example) 62 | .let { result -> 63 | ValueExampleDescriptor( 64 | name = name, 65 | value = result.value, 66 | summary = result.summary, 67 | description = result.description 68 | ) 69 | } 70 | ) 71 | 72 | 73 | /** 74 | * Determines whether this parameter is mandatory 75 | */ 76 | var required: Boolean? = null 77 | 78 | 79 | /** 80 | * Specifies that a parameter is deprecated and SHOULD be transitioned out of usage 81 | */ 82 | var deprecated: Boolean? = null 83 | 84 | 85 | /** 86 | * Sets the ability to pass empty-valued parameters. 87 | * This is valid only for query parameters and allows sending a parameter with an empty value. 88 | */ 89 | var allowEmptyValue: Boolean? = null 90 | 91 | 92 | /** 93 | * When this is true, parameter values of type array or object generate separate parameters for each value of the array or key-value 94 | * pair of the map. For other types of parameters this property has no effect 95 | */ 96 | var explode: Boolean? = null 97 | 98 | 99 | /** 100 | * Determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included 101 | * without percent-encoding. This property only applies to parameters with an in value of query 102 | */ 103 | var allowReserved: Boolean? = null 104 | 105 | 106 | /** 107 | * Describes how the parameter value will be serialized depending on the type of the parameter value. 108 | */ 109 | var style: Parameter.StyleEnum? = null 110 | 111 | 112 | /** 113 | * Don't include this parameter in the openapi-spec 114 | */ 115 | var hidden: Boolean = false 116 | 117 | 118 | /** 119 | * Build the data object for this config. 120 | */ 121 | internal fun build() = RequestParameterData( 122 | name = name, 123 | type = type, 124 | location = location, 125 | description = description, 126 | example = example, 127 | required = required ?: (location == ParameterLocation.PATH), 128 | deprecated = deprecated ?: false, 129 | allowEmptyValue = allowEmptyValue, 130 | explode = explode ?: false, 131 | allowReserved = allowReserved, 132 | style = style, 133 | hidden = hidden 134 | ) 135 | 136 | } 137 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ResponseConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.KTypeDescriptor 4 | import io.github.smiley4.ktoropenapi.config.descriptors.SwaggerTypeDescriptor 5 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 6 | import io.github.smiley4.ktoropenapi.data.ResponseData 7 | import io.swagger.v3.oas.models.media.Schema 8 | import kotlin.reflect.KType 9 | import kotlin.reflect.typeOf 10 | 11 | /** 12 | * A container for the expected responses of an operation. The container maps an HTTP response code to the expected response. 13 | * A response code can only have one response object. 14 | */ 15 | @OpenApiDslMarker 16 | class ResponseConfig internal constructor(val statusCode: String) { 17 | 18 | /** 19 | * A short description of the response 20 | */ 21 | var description: String? = null 22 | 23 | private val headers = mutableMapOf() 24 | 25 | 26 | /** 27 | * Possible headers returned with this response 28 | */ 29 | fun header(name: String, type: TypeDescriptor, block: HeaderConfig.() -> Unit = {}) { 30 | headers[name] = HeaderConfig().apply(block).apply { 31 | this.type = type 32 | } 33 | } 34 | 35 | 36 | /** 37 | * Possible headers returned with this response 38 | */ 39 | fun header(name: String, type: Schema<*>, block: HeaderConfig.() -> Unit = {}) = header(name, SwaggerTypeDescriptor(type), block) 40 | 41 | 42 | /** 43 | * Possible headers returned with this response 44 | */ 45 | fun header(name: String, type: KType, block: HeaderConfig.() -> Unit = {}) = header(name, KTypeDescriptor(type), block) 46 | 47 | 48 | /** 49 | * Possible headers returned with this response 50 | */ 51 | inline fun header(name: String, noinline block: HeaderConfig.() -> Unit = {}) = 52 | header(name, KTypeDescriptor(typeOf()), block) 53 | 54 | 55 | private var body: BaseBodyConfig? = null 56 | 57 | 58 | /** 59 | * The body returned with this response 60 | */ 61 | fun body(type: TypeDescriptor, block: SimpleBodyConfig.() -> Unit = {}) { 62 | body = SimpleBodyConfig(type).apply(block) 63 | } 64 | 65 | 66 | /** 67 | * The body returned with this response 68 | */ 69 | fun body(type: Schema<*>, block: SimpleBodyConfig.() -> Unit = {}) = body(SwaggerTypeDescriptor(type), block) 70 | 71 | 72 | /** 73 | * The body returned with this response 74 | */ 75 | fun body(type: KType, block: SimpleBodyConfig.() -> Unit = {}) = body(KTypeDescriptor(type), block) 76 | 77 | 78 | /** 79 | * The body returned with this response 80 | */ 81 | inline fun body(noinline block: SimpleBodyConfig.() -> Unit = {}) = body(KTypeDescriptor(typeOf()), block) 82 | 83 | 84 | /** 85 | * The multipart-body returned with this response 86 | */ 87 | fun multipartBody(block: MultipartBodyConfig.() -> Unit) { 88 | body = MultipartBodyConfig().apply(block) 89 | } 90 | 91 | 92 | /** 93 | * Don't include this response in the openapi-spec 94 | */ 95 | var hidden: Boolean = false 96 | 97 | 98 | /** 99 | * Build the data object for this config. 100 | */ 101 | internal fun build() = ResponseData( 102 | statusCode = statusCode, 103 | description = description, 104 | hidden = hidden, 105 | headers = headers.mapValues { it.value.build() }, 106 | body = body?.build() 107 | ) 108 | 109 | } 110 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ResponsesConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.ktor.http.HttpStatusCode 4 | 5 | /** 6 | * All possible responses of an operation 7 | */ 8 | @OpenApiDslMarker 9 | class ResponsesConfig internal constructor() { 10 | 11 | private val responses = mutableMapOf() 12 | 13 | 14 | /** 15 | * Information of response for a given http status code 16 | */ 17 | infix fun String.to(block: ResponseConfig.() -> Unit) { 18 | responses[this] = ResponseConfig(this).apply(block) 19 | } 20 | 21 | 22 | /** 23 | * Information of response for a given http status code 24 | */ 25 | infix fun HttpStatusCode.to(block: ResponseConfig.() -> Unit) = this.value.toString() to block 26 | 27 | /** 28 | * Information of response for a given http status code 29 | */ 30 | fun code(statusCode: String, block: ResponseConfig.() -> Unit) { 31 | responses[statusCode] = ResponseConfig(statusCode).apply(block) 32 | } 33 | 34 | /** 35 | * Information of response for a given http status code 36 | */ 37 | fun code(statusCode: HttpStatusCode, block: ResponseConfig.() -> Unit) = code(statusCode.value.toString(), block) 38 | 39 | 40 | /** 41 | * Information of the default response 42 | */ 43 | fun default(block: ResponseConfig.() -> Unit) = "default" to block 44 | 45 | 46 | /** 47 | * Add the given response. 48 | */ 49 | internal fun addResponse(response: ResponseConfig) { 50 | responses[response.statusCode] = response 51 | } 52 | 53 | 54 | fun getResponses() = responses.values.toList() 55 | 56 | } 57 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.KTypeDescriptor 4 | import io.github.smiley4.ktoropenapi.config.descriptors.SwaggerTypeDescriptor 5 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 6 | import io.github.smiley4.ktoropenapi.data.* 7 | import io.swagger.v3.oas.models.media.Schema 8 | import kotlin.reflect.KType 9 | import kotlin.reflect.typeOf 10 | 11 | /** 12 | * Configuration for schemas 13 | */ 14 | @OpenApiDslMarker 15 | class SchemaConfig internal constructor() { 16 | 17 | /** 18 | * The json-schema generator for all schemas. See https://github.com/SMILEY4/schema-kenerator/wiki for more information. 19 | */ 20 | var generator: GenericSchemaGenerator = SchemaConfigData.DEFAULT.generator 21 | 22 | private val schemas = mutableMapOf() 23 | 24 | /** 25 | * Add a shared schema that can be referenced by all routes by the given id. 26 | */ 27 | fun schema(schemaId: String, descriptor: TypeDescriptor) { 28 | schemas[schemaId] = descriptor 29 | } 30 | 31 | /** 32 | * Add a shared schema that can be referenced by all routes by the given id. 33 | */ 34 | fun schema(schemaId: String, schema: Schema<*>) = schema(schemaId, SwaggerTypeDescriptor(schema)) 35 | 36 | /** 37 | * Add a shared schema that can be referenced by all routes by the given id. 38 | */ 39 | fun schema(schemaId: String, schema: KType) = schema(schemaId, KTypeDescriptor(schema)) 40 | 41 | /** 42 | * Add a shared schema that can be referenced by all routes by the given id. 43 | */ 44 | inline fun schema(schemaId: String) = schema(schemaId, KTypeDescriptor(typeOf())) 45 | 46 | /** 47 | * Build the data object for this config. 48 | * @param securityConfig configuration that might contain additional schemas 49 | */ 50 | internal fun build(securityConfig: SecurityData) = SchemaConfigData( 51 | generator = generator, 52 | schemas = schemas, 53 | securitySchemas = securityConfig.defaultUnauthorizedResponse?.body?.let { body -> 54 | when (body) { 55 | is SimpleBodyData -> listOf(body.type) 56 | is MultipartBodyData -> body.parts.map { it.type } 57 | } 58 | } ?: emptyList() 59 | ) 60 | 61 | } 62 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SecurityConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.DataUtils.merge 4 | import io.github.smiley4.ktoropenapi.data.SecurityData 5 | import io.github.smiley4.ktoropenapi.data.SecuritySchemeData 6 | import io.ktor.http.HttpStatusCode 7 | 8 | /** 9 | * Configuration for security and authentication. 10 | */ 11 | @OpenApiDslMarker 12 | class SecurityConfig internal constructor() { 13 | 14 | /** 15 | * Default response to automatically add to each protected route for the "Unauthorized"-Response-Code. 16 | * Generated response can be overwritten with custom response. 17 | */ 18 | fun defaultUnauthorizedResponse(block: ResponseConfig.() -> Unit) { 19 | defaultUnauthorizedResponse = ResponseConfig(HttpStatusCode.Unauthorized.value.toString()).apply(block) 20 | } 21 | 22 | private var defaultUnauthorizedResponse: ResponseConfig? = null 23 | 24 | 25 | /** 26 | * The names of the security schemes available for use for the protected paths 27 | */ 28 | var defaultSecuritySchemeNames: Collection? = SecurityData.DEFAULT.defaultSecuritySchemeNames 29 | 30 | /** 31 | * Set the names of the security schemes available for use for the protected paths 32 | */ 33 | fun defaultSecuritySchemeNames(names: Collection) { 34 | this.defaultSecuritySchemeNames = names 35 | } 36 | 37 | /** 38 | * Set the names of the security schemes available for use for the protected paths 39 | */ 40 | fun defaultSecuritySchemeNames(vararg names: String) { 41 | this.defaultSecuritySchemeNames = names.toList() 42 | } 43 | 44 | /** 45 | * Defines security schemes that can be used by operations 46 | */ 47 | fun securityScheme(name: String, block: SecuritySchemeConfig.() -> Unit) { 48 | securitySchemes.add(SecuritySchemeConfig(name).apply(block)) 49 | } 50 | 51 | private val securitySchemes = mutableListOf() 52 | 53 | /** 54 | * Build the data object for this config. 55 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 56 | */ 57 | internal fun build(base: SecurityData) = SecurityData( 58 | defaultUnauthorizedResponse = merge(base.defaultUnauthorizedResponse, defaultUnauthorizedResponse?.build()), 59 | defaultSecuritySchemeNames = buildSet { 60 | addAll(base.defaultSecuritySchemeNames) 61 | defaultSecuritySchemeNames?.also { addAll(it) } 62 | }, 63 | securitySchemes = buildList { 64 | addAll(base.securitySchemes) 65 | addAll(securitySchemes.map { it.build(SecuritySchemeData.DEFAULT) }) 66 | } 67 | ) 68 | 69 | } 70 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SecuritySchemeConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.DataUtils.merge 4 | import io.github.smiley4.ktoropenapi.data.OpenIdOAuthFlowsData 5 | import io.github.smiley4.ktoropenapi.data.SecuritySchemeData 6 | 7 | 8 | /** 9 | * Defines a security scheme that can be used by the operations. Supported schemes are HTTP authentication, an API key (either as a header, 10 | * a cookie parameter or as a query parameter), OAuth2's common flows (implicit, password, client credentials and authorization code) 11 | */ 12 | @OpenApiDslMarker 13 | class SecuritySchemeConfig internal constructor( 14 | /** 15 | * The name of the security scheme. 16 | */ 17 | private val schemeName: String 18 | ) { 19 | 20 | /** 21 | * The type of the security scheme 22 | */ 23 | var type: AuthType? = null 24 | 25 | /** 26 | * The name scheme and of the header, query or cookie parameter to be used. 27 | */ 28 | var name: String? = null 29 | 30 | /** 31 | * The location of the API key (OpenAPI 'in'). 32 | * Required for type [AuthType.API_KEY] 33 | */ 34 | var location: AuthKeyLocation? = null 35 | 36 | 37 | /** 38 | * The name of the HTTP Authorization scheme to be used. 39 | * Required for type [AuthType.HTTP] 40 | */ 41 | var scheme: AuthScheme? = null 42 | 43 | 44 | /** 45 | * A hint to the client to identify how the bearer token is formatted. 46 | * Used for type [AuthType.HTTP] and schema [AuthScheme.BEARER] 47 | */ 48 | var bearerFormat: String? = null 49 | 50 | private var flows: OpenIdOAuthFlowsConfig? = null 51 | 52 | 53 | /** 54 | * Information for the oauth flow types supported. 55 | * Required for type [AuthType.OAUTH2] 56 | */ 57 | fun flows(block: OpenIdOAuthFlowsConfig.() -> Unit) { 58 | flows = OpenIdOAuthFlowsConfig().apply(block) 59 | } 60 | 61 | 62 | /** 63 | * OpenId Connect URL to discover OAuth2 configuration values. 64 | * Required for type [AuthType.OPENID_CONNECT] 65 | */ 66 | var openIdConnectUrl: String? = null 67 | 68 | 69 | /** 70 | * A short description of the security scheme. 71 | */ 72 | var description: String? = null 73 | 74 | /** 75 | * Build the data object for this config. 76 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 77 | */ 78 | internal fun build(base: SecuritySchemeData) = SecuritySchemeData( 79 | schemeName = schemeName, 80 | type = merge(base.type, type), 81 | name = merge(base.name, name), 82 | location = merge(base.location, location), 83 | scheme = merge(base.scheme, scheme), 84 | bearerFormat = merge(base.bearerFormat, bearerFormat), 85 | flows = flows?.build(base.flows ?: OpenIdOAuthFlowsData.DEFAULT) ?: base.flows, 86 | openIdConnectUrl = merge(base.openIdConnectUrl, openIdConnectUrl), 87 | description = merge(base.description, description), 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ServerConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.DataUtils.merge 4 | import io.github.smiley4.ktoropenapi.data.DataUtils.mergeDefault 5 | import io.github.smiley4.ktoropenapi.data.ServerData 6 | 7 | /** 8 | * An object representing a Server. 9 | */ 10 | @OpenApiDslMarker 11 | class ServerConfig internal constructor() { 12 | 13 | /** 14 | * A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to 15 | * the location where the OpenAPI document is being served 16 | */ 17 | var url: String = ServerData.DEFAULT.url 18 | 19 | /** 20 | * An optional string describing the host designated by the URL 21 | */ 22 | var description: String? = ServerData.DEFAULT.description 23 | 24 | private val variables = mutableMapOf() 25 | 26 | 27 | /** 28 | * Adds a new server variable with the given name 29 | */ 30 | fun variable(name: String, block: ServerVariableConfig.() -> Unit) { 31 | variables[name] = ServerVariableConfig(name).apply(block) 32 | } 33 | 34 | /** 35 | * Build the data object for this config. 36 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 37 | */ 38 | internal fun build(base: ServerData) = ServerData( 39 | url = mergeDefault(base.url, url, ServerData.DEFAULT.url), 40 | description = merge(base.description, description), 41 | variables = buildMap { 42 | base.variables.forEach { this[it.name] = it } 43 | variables.values.map { it.build() }.forEach { this[it.name] = it } 44 | }.values.toList() 45 | ) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ServerVariableConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.ServerVariableData 4 | 5 | /** 6 | * An object representing a Server Variable for server URL template substitution. 7 | */ 8 | @OpenApiDslMarker 9 | class ServerVariableConfig internal constructor( 10 | /** 11 | * The name of this variable 12 | */ 13 | private val name: String 14 | ) { 15 | 16 | /** 17 | * An enumeration of string values to be used if the substitution options are from a limited set. Must not be empty. 18 | */ 19 | var enum: Collection = emptyList() 20 | 21 | 22 | /** 23 | * The default value to use for substitution. Must be in the list of enums. 24 | */ 25 | var default: String? = null 26 | 27 | 28 | /** 29 | * An optional description for this server variable. 30 | */ 31 | var description: String? = null 32 | 33 | /** 34 | * Build the data object for this config. 35 | */ 36 | internal fun build() = ServerVariableData( 37 | name = name, 38 | enum = enum.toSet(), 39 | default = (default ?: enum.firstOrNull()) ?: "", 40 | description = description 41 | ) 42 | 43 | } 44 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SimpleBodyConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.ExampleDescriptor 4 | import io.github.smiley4.ktoropenapi.config.descriptors.RefExampleDescriptor 5 | import io.github.smiley4.ktoropenapi.config.descriptors.SwaggerExampleDescriptor 6 | import io.github.smiley4.ktoropenapi.config.descriptors.ValueExampleDescriptor 7 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 8 | import io.github.smiley4.ktoropenapi.data.SimpleBodyData 9 | import io.swagger.v3.oas.models.examples.Example 10 | 11 | 12 | /** 13 | * Describes the base of a single request/response body. 14 | */ 15 | @OpenApiDslMarker 16 | class SimpleBodyConfig internal constructor( 17 | /** 18 | * The type defining the schema used for the body. 19 | */ 20 | val type: TypeDescriptor, 21 | ) : BaseBodyConfig() { 22 | 23 | /** 24 | * Examples for this body 25 | */ 26 | private val examples = mutableListOf() 27 | 28 | /** 29 | * Add the given example as an example to this body 30 | */ 31 | fun example(example: ExampleDescriptor) { 32 | examples.add(example) 33 | } 34 | 35 | /** 36 | * Add the given example as an example to this body 37 | */ 38 | fun example(name: String, example: Example) = example(SwaggerExampleDescriptor(name, example)) 39 | 40 | /** 41 | * Add the given example as an example to this body 42 | */ 43 | fun example(name: String, example: ValueExampleDescriptorConfig.() -> Unit) = example( 44 | ValueExampleDescriptorConfig() 45 | .apply(example) 46 | .let { result -> 47 | ValueExampleDescriptor( 48 | name = name, 49 | value = result.value, 50 | summary = result.summary, 51 | description = result.description 52 | ) 53 | } 54 | ) 55 | 56 | 57 | /** 58 | * Add the given example as an example to this body 59 | * @param name the name of the example to display at this body 60 | * @param refName the name of the referenced example 61 | */ 62 | fun exampleRef(name: String, refName: String) = example(RefExampleDescriptor(name, refName)) 63 | 64 | /** 65 | * Add the given example as an example to this body 66 | * @param name the name of the example 67 | */ 68 | fun exampleRef(name: String) = example(RefExampleDescriptor(name, name)) 69 | 70 | override fun build() = SimpleBodyData( 71 | description = description, 72 | required = required ?: false, 73 | mediaTypes = mediaTypes.toSet(), 74 | type = type, 75 | examples = examples, 76 | ) 77 | 78 | } 79 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SpecAssigner.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | /** 4 | * Assigns (unassigned) routes to api-specs. 5 | * url - the parts of the route-url split at all `/`. 6 | * tags - the tags assigned to the route 7 | */ 8 | typealias SpecAssigner = (url: String, tags: List) -> String 9 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/TagConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.DataUtils.merge 4 | import io.github.smiley4.ktoropenapi.data.TagData 5 | 6 | /** 7 | * Adds metadata to a single tag. 8 | */ 9 | @OpenApiDslMarker 10 | class TagConfig internal constructor( 11 | /** 12 | * The name of the tag. 13 | */ 14 | var name: String 15 | ) { 16 | 17 | /** 18 | * A short description for the tag. 19 | */ 20 | var description: String? = null 21 | 22 | /** 23 | * A short description of additional external documentation for this tag. 24 | */ 25 | var externalDocDescription: String? = null 26 | 27 | /** 28 | * The URL for additional external documentation for this tag. 29 | */ 30 | var externalDocUrl: String? = null 31 | 32 | 33 | internal fun build(base: TagData) = TagData( 34 | name = name, 35 | description = merge(base.description, description), 36 | externalDocDescription = merge(base.externalDocDescription, externalDocDescription), 37 | externalDocUrl = merge(base.externalDocUrl, externalDocUrl) 38 | ) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/TagGenerator.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | /** 4 | * Generates additional tags for routes. 5 | * url - the parts of the route-url split at all `/`. 6 | * return a collection of tags. "Null"-entries will be ignored. 7 | */ 8 | typealias TagGenerator = (url: List) -> Collection 9 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/TagsConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | import io.github.smiley4.ktoropenapi.data.DataUtils.merge 4 | import io.github.smiley4.ktoropenapi.data.TagData 5 | import io.github.smiley4.ktoropenapi.data.TagsData 6 | 7 | /** 8 | * Configuration for tags 9 | */ 10 | @OpenApiDslMarker 11 | class TagsConfig internal constructor() { 12 | 13 | private val tags = mutableListOf() 14 | 15 | 16 | /** 17 | * Tags used by the specification with additional metadata. Not all tags that are used must be declared 18 | */ 19 | fun tag(name: String, block: TagConfig.() -> Unit) { 20 | tags.add(TagConfig(name).apply(block)) 21 | } 22 | 23 | 24 | /** 25 | * Automatically add tags to the route with the given url. 26 | * The returned (non-null) tags will be added to the tags specified in the route-specific documentation. 27 | */ 28 | var tagGenerator: TagGenerator = TagsData.DEFAULT.generator 29 | 30 | /** 31 | * Build the data object for this config. 32 | * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. 33 | */ 34 | internal fun build(base: TagsData) = TagsData( 35 | tags = buildList { 36 | addAll(base.tags) 37 | addAll(tags.map { it.build(TagData.DEFAULT) }) 38 | }, 39 | generator = merge(base.generator, tagGenerator) ?: TagsData.DEFAULT.generator, 40 | ) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/ValueExampleDescriptorConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config 2 | 3 | class ValueExampleDescriptorConfig { 4 | 5 | /** 6 | * the example value 7 | */ 8 | var value: Any? = null 9 | 10 | 11 | /** 12 | * a short summary of the example 13 | */ 14 | var summary: String? = null 15 | 16 | 17 | /** 18 | * a description of the example 19 | */ 20 | var description: String? = null 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/descriptors/ExampleDescriptor.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config.descriptors 2 | 3 | import io.swagger.v3.oas.models.examples.Example 4 | 5 | /** 6 | * Identifier and description of an example 7 | */ 8 | sealed class ExampleDescriptor( 9 | val name: String, 10 | ) 11 | 12 | 13 | /** 14 | * Describes an example as an object. 15 | */ 16 | class ValueExampleDescriptor( 17 | name: String, 18 | val value: Any?, 19 | val summary: String? = null, 20 | val description: String? = null, 21 | ) : ExampleDescriptor(name) 22 | 23 | 24 | /** 25 | * Describes a reference to a shared example placed in the components section 26 | * @param name the name of the example in the operation 27 | * @param refName the name/id of the example to reference in the components section 28 | */ 29 | class RefExampleDescriptor(name: String, val refName: String) : ExampleDescriptor(name) 30 | 31 | 32 | /** 33 | * Describes an example as a swagger [Example]-object 34 | */ 35 | class SwaggerExampleDescriptor(name: String, val example: Example) : ExampleDescriptor(name) 36 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/descriptors/TypeDescriptor.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.config.descriptors 2 | 3 | import io.swagger.v3.oas.models.media.Schema 4 | import kotlinx.serialization.descriptors.SerialDescriptor 5 | import kotlin.reflect.KType 6 | import kotlin.reflect.typeOf 7 | 8 | /** 9 | * Describes and identifies types and schemas. 10 | */ 11 | sealed interface TypeDescriptor 12 | 13 | 14 | /** 15 | * Describes a type from a swagger [Schema] 16 | */ 17 | class SwaggerTypeDescriptor(val schema: Schema<*>) : TypeDescriptor 18 | 19 | /** 20 | * Describes a type from a kotlin [KType] 21 | */ 22 | class KTypeDescriptor(val type: KType) : TypeDescriptor 23 | 24 | 25 | /** 26 | * Describes a type from a kotlinx-serialization [SerialDescriptor] 27 | */ 28 | class SerialTypeDescriptor(val descriptor: SerialDescriptor) : TypeDescriptor 29 | 30 | 31 | /** 32 | * Describes an array of types. 33 | */ 34 | class ArrayTypeDescriptor(val type: TypeDescriptor) : TypeDescriptor 35 | 36 | 37 | /** 38 | * Describes an object matching any of the given types. 39 | */ 40 | class AnyOfTypeDescriptor(val types: List) : TypeDescriptor 41 | 42 | 43 | /** 44 | * Describes an empty type/schema. 45 | */ 46 | class EmptyTypeDescriptor : TypeDescriptor 47 | 48 | 49 | /** 50 | * Describes a reference to a schema in the component section. 51 | */ 52 | class RefTypeDescriptor(val schemaId: String) : TypeDescriptor 53 | 54 | 55 | /** 56 | * Create a schema describing the given type parameter. 57 | */ 58 | inline fun type() = KTypeDescriptor(typeOf()) 59 | 60 | 61 | /** 62 | * Describe an empty / "any" schema. 63 | */ 64 | fun empty() = EmptyTypeDescriptor() 65 | 66 | 67 | /** 68 | * Describe a schema referenced by the given id. 69 | */ 70 | fun ref(schemaId: String) = RefTypeDescriptor(schemaId) 71 | 72 | 73 | /** 74 | * Describe an array with the given item type. 75 | */ 76 | fun array(type: TypeDescriptor) = ArrayTypeDescriptor(type) 77 | 78 | 79 | /** 80 | * Describe an array with the given item type. 81 | */ 82 | fun array(type: Schema<*>) = ArrayTypeDescriptor(SwaggerTypeDescriptor(type)) 83 | 84 | 85 | /** 86 | * Describe an array with the given item type. 87 | */ 88 | fun array(type: KType) = ArrayTypeDescriptor(KTypeDescriptor(type)) 89 | 90 | 91 | /** 92 | * Describe an array with the given item type. 93 | */ 94 | inline fun array() = ArrayTypeDescriptor(KTypeDescriptor(typeOf())) 95 | 96 | 97 | /** 98 | * Describe any of the given types. 99 | */ 100 | fun anyOf(vararg types: TypeDescriptor) = AnyOfTypeDescriptor(types.toList()) 101 | 102 | 103 | /** 104 | * Describe any of the given types. 105 | */ 106 | fun anyOf(types: Collection) = AnyOfTypeDescriptor(types.toList()) 107 | 108 | 109 | /** 110 | * Describe any of the given types. 111 | */ 112 | fun anyOf(vararg types: Schema<*>) = AnyOfTypeDescriptor(types.map { SwaggerTypeDescriptor(it) }) 113 | 114 | 115 | /** 116 | * Describe any of the given types. 117 | */ 118 | @JvmName("anyOfSwagger") 119 | fun anyOf(types: Collection>) = AnyOfTypeDescriptor(types.map { SwaggerTypeDescriptor(it) }) 120 | 121 | 122 | /** 123 | * Describe any of the given types. 124 | */ 125 | fun anyOf(vararg types: KType) = AnyOfTypeDescriptor(types.map { KTypeDescriptor(it) }) 126 | 127 | 128 | /** 129 | * Describe any of the given types. 130 | */ 131 | @JvmName("anyOfKType") 132 | fun anyOf(types: Collection) = AnyOfTypeDescriptor(types.map { KTypeDescriptor(it) }) 133 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/BaseBodyData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.ExampleDescriptor 4 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 5 | import io.ktor.http.ContentType 6 | 7 | /** 8 | * The common information for request and response bodies. 9 | */ 10 | internal sealed class BaseBodyData( 11 | val description: String?, 12 | val required: Boolean, 13 | val mediaTypes: Set, 14 | ) 15 | 16 | 17 | /** 18 | * Information for a "simple" request or response body. 19 | */ 20 | internal class SimpleBodyData( 21 | description: String?, 22 | required: Boolean, 23 | mediaTypes: Set, 24 | val type: TypeDescriptor, 25 | val examples: List 26 | ) : BaseBodyData(description, required, mediaTypes) 27 | 28 | 29 | /** 30 | * Information for a multipart request or response body. 31 | */ 32 | internal class MultipartBodyData( 33 | description: String?, 34 | required: Boolean, 35 | mediaTypes: Set, 36 | val parts: List 37 | ) : BaseBodyData(description, required, mediaTypes) 38 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/ContactData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * See [OpenAPI Specification - Contact Object](https://swagger.io/specification/#contact-object). 5 | */ 6 | internal data class ContactData( 7 | val name: String?, 8 | val url: String?, 9 | val email: String? 10 | ) { 11 | companion object { 12 | val DEFAULT = ContactData( 13 | name = null, 14 | url = null, 15 | email = null 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/DataUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | internal object DataUtils { 4 | 5 | /** 6 | * Merges the two boolean values. 7 | * @return true if "value" is true, value of "base" otherwise 8 | */ 9 | fun mergeBoolean(base: Boolean, value: Boolean) = if (value) true else base 10 | 11 | /** 12 | * Merges the two values. 13 | * @return "value" if "value" is different from the given default value, "base" otherwise 14 | */ 15 | fun mergeDefault(base: T, value: T, default: T) = if (value != default) value else base 16 | 17 | /** 18 | * Merges the two values. 19 | * @return "value" if "value" is not null, "base" otherwise 20 | */ 21 | fun merge(base: T?, value: T?) = value ?: base 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/ExampleConfigData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.ExampleDescriptor 4 | import io.github.smiley4.ktoropenapi.config.ExampleEncoder 5 | import io.github.smiley4.ktoropenapi.config.GenericExampleEncoder 6 | 7 | internal class ExampleConfigData( 8 | val sharedExamples: Map, 9 | val securityExamples: SimpleBodyData?, 10 | val exampleEncoder: GenericExampleEncoder 11 | ) { 12 | 13 | companion object { 14 | val DEFAULT = ExampleConfigData( 15 | sharedExamples = emptyMap(), 16 | securityExamples = null, 17 | exampleEncoder = ExampleEncoder.internal() 18 | ) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/ExternalDocsData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * See [OpenAPI Specification - External Documentation Object](https://swagger.io/specification/#external-documentation-object). 5 | */ 6 | internal data class ExternalDocsData( 7 | val url: String, 8 | val description: String?, 9 | ) { 10 | 11 | companion object { 12 | val DEFAULT = ExternalDocsData( 13 | url = "/", 14 | description = null 15 | ) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/HeaderData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 4 | 5 | /** 6 | * See [OpenAPI Specification - Header Object](https://swagger.io/specification/#header-object). 7 | */ 8 | internal data class HeaderData( 9 | val description: String?, 10 | val type: TypeDescriptor?, 11 | val required: Boolean, 12 | val deprecated: Boolean, 13 | val explode: Boolean?, 14 | ) 15 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/InfoData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * See [OpenAPI Specification - Info Object](https://swagger.io/specification/#info-object). 5 | */ 6 | internal data class InfoData( 7 | val title: String, 8 | val version: String?, 9 | val description: String?, 10 | val summary: String?, 11 | val termsOfService: String?, 12 | val contact: ContactData?, 13 | val license: LicenseData?, 14 | ) { 15 | companion object { 16 | val DEFAULT = InfoData( 17 | title = "API", 18 | version = null, 19 | description = null, 20 | summary = null, 21 | termsOfService = null, 22 | contact = null, 23 | license = null, 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/LicenseData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * See [OpenAPI Specification - License Object](https://swagger.io/specification/#license-object). 5 | */ 6 | internal data class LicenseData( 7 | val name: String?, 8 | val url: String?, 9 | val identifier: String? 10 | ) { 11 | companion object { 12 | val DEFAULT = LicenseData( 13 | name = null, 14 | url = null, 15 | identifier = null 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/MultipartPartData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 4 | import io.ktor.http.ContentType 5 | 6 | /** 7 | * Information about a part of a multipart request or response body. 8 | */ 9 | internal data class MultipartPartData( 10 | val name: String, 11 | val type: TypeDescriptor, 12 | val required: Boolean, 13 | val mediaTypes: Set, 14 | val headers: Map, 15 | ) 16 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/OpenApiPluginData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | import io.github.smiley4.ktoropenapi.config.OpenApiPluginConfig 4 | import io.github.smiley4.ktoropenapi.config.OutputFormat 5 | import io.github.smiley4.ktoropenapi.config.PathFilter 6 | import io.github.smiley4.ktoropenapi.config.PostBuild 7 | import io.github.smiley4.ktoropenapi.config.SpecAssigner 8 | import kotlin.reflect.KClass 9 | 10 | /** 11 | * Complete plugin configuration 12 | */ 13 | internal data class OpenApiPluginData( 14 | val specAssigner: SpecAssigner, 15 | val pathFilter: PathFilter, 16 | val ignoredRouteSelectors: Set>, 17 | val ignoredRouteSelectorClassNames: Set, 18 | val info: InfoData, 19 | val servers: List, 20 | val externalDocs: ExternalDocsData, 21 | val specConfigs: MutableMap, 22 | val postBuild: PostBuild?, 23 | val schemaConfig: SchemaConfigData, 24 | val exampleConfig: ExampleConfigData, 25 | val securityConfig: SecurityData, 26 | val tagsConfig: TagsData, 27 | val outputFormat: OutputFormat, 28 | val rootPath: String?, 29 | val autoDocumentResourcesRoutes: Boolean, 30 | ) { 31 | 32 | companion object { 33 | val DEFAULT = OpenApiPluginData( 34 | specAssigner = { _, _ -> OpenApiPluginConfig.DEFAULT_SPEC_ID }, 35 | pathFilter = { _, _ -> true }, 36 | ignoredRouteSelectors = emptySet(), 37 | ignoredRouteSelectorClassNames = emptySet(), 38 | info = InfoData.DEFAULT, 39 | servers = emptyList(), 40 | externalDocs = ExternalDocsData.DEFAULT, 41 | specConfigs = mutableMapOf(), 42 | postBuild = null, 43 | schemaConfig = SchemaConfigData.DEFAULT, 44 | exampleConfig = ExampleConfigData.DEFAULT, 45 | securityConfig = SecurityData.DEFAULT, 46 | tagsConfig = TagsData.DEFAULT, 47 | outputFormat = OutputFormat.JSON, 48 | rootPath = null, 49 | autoDocumentResourcesRoutes = false, 50 | ) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/OpenIdOAuthFlowData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * See [OpenAPI Specification - OAuth Flow Object](https://swagger.io/specification/#oauth-flow-object). 5 | */ 6 | internal data class OpenIdOAuthFlowData( 7 | val authorizationUrl: String? = null, 8 | val tokenUrl: String? = null, 9 | val refreshUrl: String? = null, 10 | val scopes: Map? = null, 11 | ) { 12 | 13 | companion object { 14 | val DEFAULT = OpenIdOAuthFlowData( 15 | authorizationUrl = null, 16 | tokenUrl = null, 17 | refreshUrl = null, 18 | scopes = null, 19 | ) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/OpenIdOAuthFlowsData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * See [OpenAPI Specification - OAuth Flows Object](https://swagger.io/specification/#oauth-flows-object). 5 | */ 6 | internal data class OpenIdOAuthFlowsData( 7 | val implicit: OpenIdOAuthFlowData?, 8 | val password: OpenIdOAuthFlowData?, 9 | val clientCredentials: OpenIdOAuthFlowData?, 10 | val authorizationCode: OpenIdOAuthFlowData?, 11 | ) { 12 | 13 | companion object { 14 | val DEFAULT = OpenIdOAuthFlowsData( 15 | implicit = null, 16 | password = null, 17 | clientCredentials = null, 18 | authorizationCode = null, 19 | ) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/RequestData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * Information about a request 5 | */ 6 | internal data class RequestData( 7 | val parameters: List, 8 | val body: BaseBodyData?, 9 | ) 10 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/RequestParameterData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | import io.github.smiley4.ktoropenapi.config.descriptors.ExampleDescriptor 4 | import io.github.smiley4.ktoropenapi.config.ParameterLocation 5 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 6 | import io.swagger.v3.oas.models.parameters.Parameter 7 | 8 | /** 9 | * Information about a request (query, path or header) parameter. 10 | */ 11 | internal data class RequestParameterData( 12 | val name: String, 13 | val type: TypeDescriptor, 14 | val location: ParameterLocation, 15 | val description: String?, 16 | val example: ExampleDescriptor?, 17 | val required: Boolean, 18 | val deprecated: Boolean, 19 | val allowEmptyValue: Boolean?, 20 | val explode: Boolean, 21 | val allowReserved: Boolean?, 22 | val style: Parameter.StyleEnum?, 23 | val hidden: Boolean, 24 | ) 25 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/ResponseData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * Information about a response for a status-code. 5 | */ 6 | internal data class ResponseData( 7 | val statusCode: String, 8 | val description: String?, 9 | val hidden: Boolean, 10 | val headers: Map, 11 | val body: BaseBodyData?, 12 | ) 13 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/RouteData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * Information about a single route. 5 | */ 6 | internal data class RouteData( 7 | val specName: String?, 8 | val tags: Set, 9 | val summary: String?, 10 | val description: String?, 11 | val operationId: String?, 12 | val deprecated: Boolean, 13 | val hidden: Boolean, 14 | val securitySchemeNames: List, 15 | val protected: Boolean?, 16 | val request: RequestData, 17 | val responses: List, 18 | val externalDocs: ExternalDocsData?, 19 | val servers: List, 20 | ) 21 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/SchemaConfigData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | import io.github.smiley4.ktoropenapi.config.GenericSchemaGenerator 4 | import io.github.smiley4.ktoropenapi.config.SchemaGenerator 5 | import io.github.smiley4.ktoropenapi.config.descriptors.TypeDescriptor 6 | 7 | /** 8 | * Common configuration for schemas. 9 | */ 10 | internal data class SchemaConfigData( 11 | val schemas: Map, 12 | val generator: GenericSchemaGenerator, 13 | val securitySchemas: List 14 | ) { 15 | companion object { 16 | val DEFAULT = SchemaConfigData( 17 | schemas = emptyMap(), 18 | generator = SchemaGenerator.reflection(), 19 | securitySchemas = emptyList() 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/SecurityData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * Common security configuration information. 5 | */ 6 | internal data class SecurityData( 7 | val defaultUnauthorizedResponse: ResponseData?, 8 | val defaultSecuritySchemeNames: Set, 9 | val securitySchemes: List, 10 | ) { 11 | companion object { 12 | val DEFAULT = SecurityData( 13 | defaultUnauthorizedResponse = null, 14 | defaultSecuritySchemeNames = emptySet(), 15 | securitySchemes = emptyList() 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/SecuritySchemeData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | import io.github.smiley4.ktoropenapi.config.AuthKeyLocation 4 | import io.github.smiley4.ktoropenapi.config.AuthScheme 5 | import io.github.smiley4.ktoropenapi.config.AuthType 6 | 7 | /** 8 | * See [OpenAPI Specification - Security Scheme Object](https://swagger.io/specification/#security-scheme-object). 9 | */ 10 | internal data class SecuritySchemeData( 11 | val schemeName: String, 12 | val type: AuthType?, 13 | val name: String?, 14 | val location: AuthKeyLocation?, 15 | val scheme: AuthScheme?, 16 | val bearerFormat: String?, 17 | val flows: OpenIdOAuthFlowsData?, 18 | val openIdConnectUrl: String?, 19 | val description: String? 20 | ) { 21 | 22 | companion object { 23 | val DEFAULT = SecuritySchemeData( 24 | schemeName = "", 25 | type = null, 26 | name = null, 27 | location = null, 28 | scheme = null, 29 | bearerFormat = null, 30 | flows = null, 31 | openIdConnectUrl = null, 32 | description = null, 33 | ) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/ServerData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * See [OpenAPI Specification - Server Object](https://swagger.io/specification/#server-object). 5 | */ 6 | internal data class ServerData( 7 | val url: String, 8 | val description: String?, 9 | val variables: List 10 | ) { 11 | 12 | companion object { 13 | val DEFAULT = ServerData( 14 | url = "/", 15 | description = null, 16 | variables = emptyList() 17 | ) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/ServerVariableData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | 4 | /** 5 | * See [OpenAPI Specification - Server Variable Object](https://swagger.io/specification/#server-variable-object). 6 | */ 7 | internal data class ServerVariableData( 8 | val name: String, 9 | val enum: Set, 10 | val default: String, 11 | val description: String? 12 | ) 13 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/TagData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | /** 4 | * See [OpenAPI Specification - Tag Object](https://swagger.io/specification/#tag-object). 5 | */ 6 | internal data class TagData( 7 | val name: String, 8 | val description: String?, 9 | val externalDocDescription: String?, 10 | val externalDocUrl: String? 11 | ) { 12 | 13 | companion object { 14 | val DEFAULT = TagData( 15 | name = "", 16 | description = null, 17 | externalDocDescription = null, 18 | externalDocUrl = null 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/TagsData.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktoropenapi.data 2 | 3 | import io.github.smiley4.ktoropenapi.config.TagGenerator 4 | 5 | /** 6 | * Common configuration for tags. 7 | */ 8 | internal data class TagsData( 9 | val tags: List, 10 | val generator: TagGenerator, 11 | ) { 12 | 13 | companion object { 14 | val DEFAULT = TagsData( 15 | tags = emptyList(), 16 | generator = { emptyList() } 17 | ) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/resources/documentationExtractor.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package io.github.smiley4.ktoropenapi.resources 4 | 5 | import io.github.smiley4.ktoropenapi.OpenApiPlugin 6 | import io.github.smiley4.ktoropenapi.config.ParameterLocation 7 | import io.github.smiley4.ktoropenapi.config.RouteConfig 8 | import io.github.smiley4.ktoropenapi.config.descriptors.SerialTypeDescriptor 9 | import io.ktor.resources.serialization.ResourcesFormat 10 | import kotlinx.serialization.ExperimentalSerializationApi 11 | import kotlinx.serialization.KSerializer 12 | import kotlinx.serialization.descriptors.SerialDescriptor 13 | import kotlinx.serialization.descriptors.StructureKind 14 | import kotlinx.serialization.descriptors.elementNames 15 | 16 | private data class ParameterData( 17 | val name: String, 18 | val descriptor: SerialDescriptor, 19 | val optional: Boolean, 20 | val location: ParameterLocation 21 | ) 22 | 23 | 24 | /** 25 | * Intended for internal use. 26 | * Automatically extract route configuration from the given resource class. 27 | */ 28 | fun extractTypesafeDocumentation(serializer: KSerializer, resourcesFormat: ResourcesFormat): RouteConfig.() -> Unit { 29 | if(!OpenApiPlugin.config.autoDocumentResourcesRoutes) { 30 | return {} 31 | } 32 | // Note: typesafe routing only defines information about path & query parameters - no other information is available 33 | val path = resourcesFormat.encodeToPathPattern(serializer) 34 | return { 35 | request { 36 | collectParameters(serializer.descriptor, path).forEach { parameter -> 37 | parameter(parameter.location, parameter.name, SerialTypeDescriptor(parameter.descriptor)) { 38 | required = !parameter.optional 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | 46 | 47 | private fun collectParameters(descriptor: SerialDescriptor, path: String): List { 48 | val parameters = mutableListOf() 49 | 50 | descriptor.elementNames.forEach { name -> 51 | val index = descriptor.getElementIndex(name) 52 | val elementDescriptor = descriptor.getElementDescriptor(index) 53 | if (!elementDescriptor.isInline && elementDescriptor.kind is StructureKind.CLASS) { 54 | parameters.addAll(collectParameters(elementDescriptor, path)) 55 | } else { 56 | val location = getLocation(name, path) 57 | parameters.add( 58 | ParameterData( 59 | name = name, 60 | descriptor = elementDescriptor, 61 | optional = when(location) { 62 | ParameterLocation.PATH -> path.contains("{$name?}") 63 | ParameterLocation.QUERY -> elementDescriptor.isNullable || descriptor.isElementOptional(index) 64 | else -> false 65 | }, 66 | location = location 67 | ) 68 | ) 69 | } 70 | } 71 | 72 | return parameters 73 | } 74 | 75 | 76 | /** 77 | * path contains parameter with given name -> "PATH" 78 | * path does not contain parameter with given name -> "QUERY" 79 | */ 80 | private fun getLocation(name: String, path: String): ParameterLocation { 81 | return if (path.contains("{$name}") || path.contains("{$name?}") || path.contains("{$name...}")) { 82 | ParameterLocation.PATH 83 | } else { 84 | ParameterLocation.QUERY 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ExternalDocsBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui.builder 2 | 3 | import io.github.smiley4.ktoropenapi.builder.openapi.ExternalDocumentationBuilder 4 | import io.github.smiley4.ktoropenapi.data.ExternalDocsData 5 | import io.github.smiley4.ktoropenapi.config.ExternalDocsConfig 6 | import io.kotest.core.spec.style.StringSpec 7 | import io.kotest.matchers.shouldBe 8 | import io.swagger.v3.oas.models.ExternalDocumentation 9 | 10 | class ExternalDocsBuilderTest : StringSpec({ 11 | 12 | "default external docs object" { 13 | buildExternalDocsObject {}.also { docs -> 14 | docs.url shouldBe "/" 15 | docs.description shouldBe null 16 | } 17 | } 18 | 19 | "complete server object" { 20 | buildExternalDocsObject { 21 | url = "Test URL" 22 | description = "Test Description" 23 | }.also { docs -> 24 | docs.url shouldBe "Test URL" 25 | docs.description shouldBe "Test Description" 26 | } 27 | } 28 | 29 | }) { 30 | 31 | companion object { 32 | 33 | private fun buildExternalDocsObject(builder: ExternalDocsConfig.() -> Unit): ExternalDocumentation { 34 | return ExternalDocumentationBuilder().build(ExternalDocsConfig().apply(builder).build(ExternalDocsData.DEFAULT)) 35 | } 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/InfoBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui.builder 2 | import io.github.smiley4.ktoropenapi.builder.openapi.ContactBuilder 3 | import io.github.smiley4.ktoropenapi.builder.openapi.InfoBuilder 4 | import io.github.smiley4.ktoropenapi.builder.openapi.LicenseBuilder 5 | import io.github.smiley4.ktoropenapi.data.InfoData 6 | import io.github.smiley4.ktoropenapi.config.InfoConfig 7 | import io.kotest.core.spec.style.StringSpec 8 | import io.kotest.matchers.nulls.shouldNotBeNull 9 | import io.kotest.matchers.shouldBe 10 | import io.swagger.v3.oas.models.info.Info 11 | 12 | 13 | class InfoBuilderTest : StringSpec({ 14 | 15 | "empty info object" { 16 | buildInfoObject {}.also { info -> 17 | info.title shouldBe "API" 18 | info.version shouldBe "latest" 19 | info.description shouldBe null 20 | info.termsOfService shouldBe null 21 | info.contact shouldBe null 22 | info.license shouldBe null 23 | info.extensions shouldBe null 24 | info.summary shouldBe null 25 | } 26 | } 27 | 28 | "full info object" { 29 | buildInfoObject { 30 | title = "Test Api" 31 | version = "1.0" 32 | summary = "testing api" 33 | description = "Api for testing" 34 | termsOfService = "test-tos" 35 | contact { 36 | name = "Test Person" 37 | url = "example.com" 38 | email = "test.mail" 39 | 40 | } 41 | license { 42 | name = "Test License" 43 | url = "example.com" 44 | identifier = "Example" 45 | } 46 | }.also { info -> 47 | info.title shouldBe "Test Api" 48 | info.version shouldBe "1.0" 49 | info.summary shouldBe "testing api" 50 | info.description shouldBe "Api for testing" 51 | info.termsOfService shouldBe "test-tos" 52 | info.contact 53 | .also { contact -> contact.shouldNotBeNull() } 54 | ?.also { contact -> 55 | contact.name shouldBe "Test Person" 56 | contact.url shouldBe "example.com" 57 | contact.email shouldBe "test.mail" 58 | } 59 | info.license 60 | .also { license -> license.shouldNotBeNull() } 61 | ?.also { license -> 62 | license.name shouldBe "Test License" 63 | license.url shouldBe "example.com" 64 | license.identifier shouldBe "Example" 65 | } 66 | info.extensions shouldBe null 67 | } 68 | } 69 | 70 | }) { 71 | 72 | companion object { 73 | 74 | private fun buildInfoObject(builder: InfoConfig.() -> Unit): Info { 75 | return InfoBuilder( 76 | contactBuilder = ContactBuilder(), 77 | licenseBuilder = LicenseBuilder() 78 | ).build(InfoConfig().apply(builder).build(InfoData.DEFAULT)) 79 | } 80 | 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ServersBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui.builder 2 | 3 | import io.github.smiley4.ktoropenapi.builder.openapi.ServerBuilder 4 | import io.github.smiley4.ktoropenapi.data.ServerData 5 | import io.github.smiley4.ktoropenapi.config.ServerConfig 6 | import io.kotest.core.spec.style.StringSpec 7 | import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder 8 | import io.kotest.matchers.shouldBe 9 | import io.swagger.v3.oas.models.servers.Server 10 | 11 | class ServersBuilderTest : StringSpec({ 12 | 13 | "default server object" { 14 | buildServerObject {}.also { server -> 15 | server.url shouldBe "/" 16 | server.description shouldBe null 17 | server.variables shouldBe null 18 | server.extensions shouldBe null 19 | } 20 | } 21 | 22 | "complete server object" { 23 | buildServerObject { 24 | url = "Test URL" 25 | description = "Test Description" 26 | variable("version") { 27 | description = "the version of the api" 28 | default = "2" 29 | enum = setOf("1", "2", "3") 30 | } 31 | variable("region") { 32 | description = "the region of the api" 33 | default = "somewhere" 34 | enum = setOf("somewhere", "else") 35 | } 36 | }.also { server -> 37 | server.url shouldBe "Test URL" 38 | server.description shouldBe "Test Description" 39 | server.variables.keys shouldContainExactlyInAnyOrder listOf("version", "region") 40 | server.variables["version"]!!.also { variable -> 41 | variable.description shouldBe "the version of the api" 42 | variable.default shouldBe "2" 43 | variable.enum shouldContainExactlyInAnyOrder listOf("1", "2", "3") 44 | } 45 | server.variables["region"]!!.also { variable -> 46 | variable.description shouldBe "the region of the api" 47 | variable.default shouldBe "somewhere" 48 | variable.enum shouldContainExactlyInAnyOrder listOf("somewhere", "else") 49 | } 50 | server.extensions shouldBe null 51 | } 52 | } 53 | 54 | }) { 55 | 56 | companion object { 57 | 58 | private fun buildServerObject(builder: ServerConfig.() -> Unit): Server { 59 | return ServerBuilder().build(ServerConfig().apply(builder).build(ServerData.DEFAULT)) 60 | } 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/TagsBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui.builder 2 | 3 | import io.github.smiley4.ktoropenapi.builder.openapi.TagBuilder 4 | import io.github.smiley4.ktoropenapi.builder.openapi.TagExternalDocumentationBuilder 5 | import io.github.smiley4.ktoropenapi.data.TagData 6 | import io.github.smiley4.ktoropenapi.config.TagConfig 7 | import io.kotest.core.spec.style.StringSpec 8 | import io.kotest.matchers.nulls.shouldNotBeNull 9 | import io.kotest.matchers.shouldBe 10 | import io.swagger.v3.oas.models.tags.Tag 11 | 12 | 13 | class TagsBuilderTest : StringSpec({ 14 | 15 | "empty tag object" { 16 | buildTagObject("test-tag") {}.also { tag -> 17 | tag.name shouldBe "test-tag" 18 | tag.description shouldBe null 19 | tag.externalDocs shouldBe null 20 | tag.extensions shouldBe null 21 | } 22 | } 23 | 24 | "full tag object" { 25 | buildTagObject("test-tag") { 26 | description = "Description of tag" 27 | externalDocDescription = "Description of external docs" 28 | externalDocUrl = "example.com" 29 | }.also { tag -> 30 | tag.name shouldBe "test-tag" 31 | tag.description shouldBe "Description of tag" 32 | tag.externalDocs 33 | .also { docs -> docs.shouldNotBeNull() } 34 | ?.also { docs -> 35 | docs.description shouldBe "Description of external docs" 36 | docs.url shouldBe "example.com" 37 | docs.extensions shouldBe null 38 | } 39 | tag.extensions shouldBe null 40 | } 41 | } 42 | 43 | }) { 44 | 45 | companion object { 46 | 47 | private fun buildTagObject(name: String, builder: TagConfig.() -> Unit): Tag { 48 | return TagBuilder( 49 | tagExternalDocumentationBuilder = TagExternalDocumentationBuilder() 50 | ).build(TagConfig(name).apply(builder).build(TagData.DEFAULT)) 51 | } 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/misc/RoutingTests.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui.misc 2 | 3 | import io.github.smiley4.ktoropenapi.OpenApi 4 | import io.github.smiley4.ktoropenapi.config.OutputFormat 5 | import io.github.smiley4.ktoropenapi.openApi 6 | import io.kotest.matchers.shouldBe 7 | import io.kotest.matchers.string.shouldNotBeEmpty 8 | import io.kotest.matchers.string.shouldStartWith 9 | import io.ktor.client.HttpClient 10 | import io.ktor.client.request.get 11 | import io.ktor.client.statement.bodyAsText 12 | import io.ktor.http.ContentType 13 | import io.ktor.http.HttpStatusCode 14 | import io.ktor.http.contentType 15 | import io.ktor.server.response.respondText 16 | import io.ktor.server.routing.get 17 | import io.ktor.server.routing.route 18 | import io.ktor.server.testing.testApplication 19 | import kotlin.test.Test 20 | 21 | class RoutingTests { 22 | 23 | @Test 24 | fun jsonSpec() = openApiTestApplication { 25 | get("hello").also { 26 | it.status shouldBe HttpStatusCode.OK 27 | it.body shouldBe "Hello Test" 28 | } 29 | get("/").also { 30 | it.status shouldBe HttpStatusCode.NotFound 31 | } 32 | get("/api.json").also { 33 | it.status shouldBe HttpStatusCode.OK 34 | it.contentType shouldBe ContentType.Application.Json 35 | it.body.shouldNotBeEmpty() 36 | it.body shouldStartWith "{\n \"openapi\" : \"3.1.0\"," 37 | } 38 | } 39 | 40 | 41 | @Test 42 | fun yamlSpec() = openApiTestApplication(OutputFormat.YAML) { 43 | get("hello").also { 44 | it.status shouldBe HttpStatusCode.OK 45 | it.body shouldBe "Hello Test" 46 | } 47 | get("/").also { 48 | it.status shouldBe HttpStatusCode.NotFound 49 | } 50 | get("/api.yml").also { 51 | it.status shouldBe HttpStatusCode.OK 52 | it.contentType shouldBe ContentType.Text.Plain.withParameter("charset", "utf-8") 53 | it.body.shouldNotBeEmpty() 54 | it.body shouldStartWith "openapi: 3.1.0\n" 55 | } 56 | } 57 | 58 | private fun openApiTestApplication(format: OutputFormat = OutputFormat.JSON, block: suspend TestContext.() -> Unit) { 59 | testApplication { 60 | val client = createClient { 61 | this.followRedirects = false 62 | } 63 | install(OpenApi) { 64 | outputFormat = format 65 | } 66 | routing { 67 | val routeSuffix = when(format) { 68 | OutputFormat.JSON -> "json" 69 | OutputFormat.YAML -> "yml" 70 | } 71 | route("api.$routeSuffix") { 72 | openApi() 73 | } 74 | get("hello") { 75 | call.respondText("Hello Test") 76 | } 77 | } 78 | TestContext(client).apply { block() } 79 | } 80 | } 81 | 82 | class TestContext(private val client: HttpClient) { 83 | 84 | suspend fun get(path: String): GetResult { 85 | return client.get(path) 86 | .let { 87 | GetResult( 88 | path = path, 89 | status = it.status, 90 | contentType = it.contentType(), 91 | body = it.bodyAsText(), 92 | redirect = it.headers["Location"] 93 | ) 94 | } 95 | .also { it.print() } 96 | } 97 | 98 | 99 | private fun GetResult.print() { 100 | println("GET ${this.path} => ${this.status} (${this.contentType}): ${this.body}") 101 | } 102 | } 103 | 104 | data class GetResult( 105 | val path: String, 106 | val status: HttpStatusCode, 107 | val contentType: ContentType?, 108 | val body: String, 109 | val redirect: String? 110 | ) 111 | 112 | } 113 | -------------------------------------------------------------------------------- /ktor-openapi/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %cyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) | %highlight(%-5.5level{5}) | %gray(%-16.16thread{16}) | %magenta(%-25.25logger{25}) | %m%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ktor-redoc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.JavadocJar 2 | import com.vanniktech.maven.publish.KotlinJvm 3 | import com.vanniktech.maven.publish.SonatypeHost 4 | import io.gitlab.arturbosch.detekt.Detekt 5 | import org.jetbrains.dokka.gradle.DokkaTask 6 | 7 | val projectGroupId: String by project 8 | val projectVersion: String by project 9 | group = projectGroupId 10 | version = projectVersion 11 | 12 | plugins { 13 | kotlin("jvm") 14 | id("org.owasp.dependencycheck") 15 | id("com.github.ben-manes.versions") 16 | id("io.gitlab.arturbosch.detekt") 17 | id("com.vanniktech.maven.publish") 18 | id("org.jetbrains.dokka") 19 | } 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | val versionKtor: String by project 27 | implementation("io.ktor:ktor-server-core-jvm:$versionKtor") 28 | implementation("io.ktor:ktor-server-content-negotiation:$versionKtor") 29 | testImplementation("io.ktor:ktor-server-netty-jvm:$versionKtor") 30 | testImplementation("io.ktor:ktor-server-content-negotiation:$versionKtor") 31 | testImplementation("io.ktor:ktor-serialization-jackson:$versionKtor") 32 | testImplementation("io.ktor:ktor-server-test-host:$versionKtor") 33 | 34 | val versionRedoc: String by project 35 | implementation("org.webjars:redoc:$versionRedoc") 36 | 37 | val versionKotest: String by project 38 | testImplementation("io.kotest:kotest-runner-junit5:$versionKotest") 39 | testImplementation("io.kotest:kotest-assertions-core:$versionKotest") 40 | 41 | val versionKotlinTest: String by project 42 | testImplementation("org.jetbrains.kotlin:kotlin-test:$versionKotlinTest") 43 | } 44 | 45 | kotlin { 46 | jvmToolchain(11) 47 | } 48 | 49 | tasks.withType().configureEach { 50 | useJUnitPlatform() 51 | } 52 | 53 | detekt { 54 | ignoreFailures = false 55 | buildUponDefaultConfig = true 56 | allRules = false 57 | config.setFrom("$projectDir/../detekt/detekt.yml") 58 | } 59 | tasks.withType().configureEach { 60 | reports { 61 | html.required.set(true) 62 | md.required.set(true) 63 | xml.required.set(false) 64 | txt.required.set(false) 65 | sarif.required.set(false) 66 | } 67 | } 68 | 69 | tasks.withType().configureEach { 70 | outputDirectory.set(file("$rootDir/docs/dokka/ktor-redoc")) 71 | } 72 | 73 | mavenPublishing { 74 | val projectGroupId: String by project 75 | val projectVersion: String by project 76 | val projectBaseScmUrl: String by project 77 | val projectBaseScmConnection: String by project 78 | val projectLicenseName: String by project 79 | val projectLicenseUrl: String by project 80 | val projectDeveloperName: String by project 81 | val projectDeveloperUrl: String by project 82 | 83 | configure(KotlinJvm(JavadocJar.Dokka("dokkaHtml"), true)) 84 | publishToMavenCentral(SonatypeHost.S01) 85 | signAllPublications() 86 | coordinates(projectGroupId, "ktor-redoc", projectVersion) 87 | pom { 88 | name.set("Ktor Redoc") 89 | description.set("Ktor plugin to provide Redoc") 90 | url.set(projectBaseScmUrl +"ktor-redoc") 91 | licenses { 92 | license { 93 | name.set(projectLicenseName) 94 | url.set(projectLicenseUrl) 95 | distribution.set(projectLicenseUrl) 96 | } 97 | } 98 | scm { 99 | url.set(projectBaseScmUrl + "ktor-redoc") 100 | connection.set(projectBaseScmConnection + "ktor-redoc.git") 101 | } 102 | developers { 103 | developer { 104 | id.set(projectDeveloperName) 105 | name.set(projectDeveloperName) 106 | url.set(projectDeveloperUrl) 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /ktor-redoc/src/main/kotlin/io/github/smiley4/ktorredoc/PropertyBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorredoc 2 | 3 | 4 | 5 | internal class PropertyBuilder { 6 | 7 | data class Raw(val value: String) 8 | 9 | private val entries = mutableMapOf() 10 | 11 | operator fun set(key: String, value: Any?) { 12 | entries[key] = value 13 | } 14 | 15 | 16 | /** 17 | * @param prefix string to add before each line 18 | * @param separator the separator string to join each line with 19 | * @param nullBehavior how to handle null values. Valid values are "include", "remove", "to_undefined" 20 | */ 21 | fun render(prefix: String, separator: String, nullBehavior: String): String { 22 | return entries 23 | .mapValues { (_, value) -> 24 | when (value) { 25 | null -> when(nullBehavior) { 26 | "include" -> "null" 27 | "remove" -> null 28 | "to_undefined" -> "undefined" 29 | else -> throw IllegalArgumentException("Invalid value for null behaviour: '$nullBehavior'") 30 | } 31 | is Raw -> value.value 32 | is Number -> value.toString() 33 | is Boolean -> value.toString() 34 | is String -> "'$value'" 35 | else -> "'$value'" 36 | } 37 | } 38 | .filterValues { it != null } 39 | .mapValues { (_, value) -> value!! } 40 | .map { (key, value) -> "$prefix$key=$value" } 41 | .joinToString(separator) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /ktor-redoc/src/main/kotlin/io/github/smiley4/ktorredoc/RedocRouteSelector.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorredoc 2 | 3 | import io.ktor.server.routing.Route 4 | import io.ktor.server.routing.RouteSelector 5 | import io.ktor.server.routing.RouteSelectorEvaluation 6 | import io.ktor.server.routing.RoutingResolveContext 7 | 8 | internal class RedocRouteSelector : RouteSelector() { 9 | override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int) = RouteSelectorEvaluation.Transparent 10 | } 11 | 12 | internal fun Route.markedRedoc(build: Route.() -> Unit): Route { 13 | return createChild(RedocRouteSelector()).also(build) 14 | } 15 | -------------------------------------------------------------------------------- /ktor-redoc/src/main/kotlin/io/github/smiley4/ktorredoc/ResourceContent.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorredoc 2 | 3 | import io.ktor.http.ContentType 4 | import io.ktor.http.content.OutgoingContent 5 | import java.net.URL 6 | 7 | internal class ResourceContent(private val resource: URL) : OutgoingContent.ByteArrayContent() { 8 | 9 | private val contentTypes = mapOf( 10 | "html" to ContentType.Text.Html, 11 | "css" to ContentType.Text.CSS, 12 | "js" to ContentType.Application.JavaScript, 13 | "json" to ContentType.Application.Json, 14 | "png" to ContentType.Image.PNG 15 | ) 16 | 17 | private val bytes by lazy { resource.readBytes() } 18 | 19 | override val contentType: ContentType? by lazy { 20 | val extension = resource.file.substring(resource.file.lastIndexOf('.') + 1) 21 | contentTypes[extension] ?: ContentType.Text.Html 22 | } 23 | 24 | override val contentLength: Long? by lazy { 25 | bytes.size.toLong() 26 | } 27 | 28 | override fun bytes(): ByteArray = bytes 29 | 30 | override fun toString() = "ResourceContent \"$resource\"" 31 | } 32 | -------------------------------------------------------------------------------- /ktor-redoc/src/test/kotlin/io/github/smiley4/ktorredoc/RoutingTests.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorredoc 2 | 3 | import io.github.smiley4.ktorredoc.config.RedocConfig 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.matchers.string.shouldNotBeEmpty 6 | import io.ktor.client.HttpClient 7 | import io.ktor.client.request.* 8 | import io.ktor.client.statement.bodyAsText 9 | import io.ktor.http.ContentType 10 | import io.ktor.http.HttpStatusCode 11 | import io.ktor.http.contentType 12 | import io.ktor.http.withCharset 13 | import io.ktor.server.response.respondText 14 | import io.ktor.server.routing.get 15 | import io.ktor.server.routing.route 16 | import io.ktor.server.testing.testApplication 17 | import kotlin.test.Test 18 | 19 | class RoutingTests { 20 | 21 | @Test 22 | fun defaultConfig() = redocTestApplication { 23 | get("hello").also { 24 | it.status shouldBe HttpStatusCode.OK 25 | it.body shouldBe "Hello Test" 26 | } 27 | get("/redoc").also { 28 | it.status shouldBe HttpStatusCode.Found 29 | it.redirect shouldBe "/redoc/index.html" 30 | } 31 | get("/redoc/index.html").also { 32 | it.status shouldBe HttpStatusCode.OK 33 | it.contentType shouldBe ContentType.Text.Html.withCharset(java.nio.charset.Charset.forName("UTF-8")) 34 | it.body.shouldNotBeEmpty() 35 | } 36 | } 37 | 38 | private fun redocTestApplication(config: RedocConfig.() -> Unit = {}, block: suspend TestContext.() -> Unit = {}) { 39 | testApplication { 40 | val client = createClient { 41 | this.followRedirects = false 42 | } 43 | routing { 44 | route("redoc") { 45 | redoc("api.json", config) 46 | } 47 | get("hello") { 48 | call.respondText("Hello Test") 49 | } 50 | } 51 | TestContext(client).apply { block() } 52 | } 53 | } 54 | 55 | class TestContext(private val client: HttpClient) { 56 | 57 | suspend fun get(path: String): GetResult { 58 | return client.get(path) 59 | .let { 60 | GetResult( 61 | path = path, 62 | status = it.status, 63 | contentType = it.contentType(), 64 | body = it.bodyAsText(), 65 | redirect = it.headers["Location"] 66 | ) 67 | } 68 | .also { it.print() } 69 | } 70 | 71 | 72 | private fun GetResult.print() { 73 | println("GET ${this.path} => ${this.status} (${this.contentType}): ${this.body}") 74 | } 75 | } 76 | 77 | data class GetResult( 78 | val path: String, 79 | val status: HttpStatusCode, 80 | val contentType: ContentType?, 81 | val body: String, 82 | val redirect: String? 83 | ) 84 | 85 | } 86 | -------------------------------------------------------------------------------- /ktor-swagger-ui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.JavadocJar 2 | import com.vanniktech.maven.publish.KotlinJvm 3 | import com.vanniktech.maven.publish.SonatypeHost 4 | import io.gitlab.arturbosch.detekt.Detekt 5 | import org.jetbrains.dokka.gradle.DokkaTask 6 | 7 | val projectGroupId: String by project 8 | val projectVersion: String by project 9 | group = projectGroupId 10 | version = projectVersion 11 | 12 | plugins { 13 | kotlin("jvm") 14 | id("org.owasp.dependencycheck") 15 | id("com.github.ben-manes.versions") 16 | id("io.gitlab.arturbosch.detekt") 17 | id("com.vanniktech.maven.publish") 18 | id("org.jetbrains.dokka") 19 | } 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | val versionKtor: String by project 27 | implementation("io.ktor:ktor-server-core-jvm:$versionKtor") 28 | implementation("io.ktor:ktor-server-content-negotiation:$versionKtor") 29 | testImplementation("io.ktor:ktor-server-netty-jvm:$versionKtor") 30 | testImplementation("io.ktor:ktor-server-content-negotiation:$versionKtor") 31 | testImplementation("io.ktor:ktor-serialization-jackson:$versionKtor") 32 | testImplementation("io.ktor:ktor-server-test-host:$versionKtor") 33 | 34 | val versionSwaggerUI: String by project 35 | implementation("org.webjars:swagger-ui:$versionSwaggerUI") 36 | 37 | val versionKotest: String by project 38 | testImplementation("io.kotest:kotest-runner-junit5:$versionKotest") 39 | testImplementation("io.kotest:kotest-assertions-core:$versionKotest") 40 | 41 | val versionKotlinTest: String by project 42 | testImplementation("org.jetbrains.kotlin:kotlin-test:$versionKotlinTest") 43 | } 44 | 45 | kotlin { 46 | jvmToolchain(11) 47 | } 48 | 49 | tasks.withType().configureEach { 50 | useJUnitPlatform() 51 | } 52 | 53 | detekt { 54 | ignoreFailures = false 55 | buildUponDefaultConfig = true 56 | allRules = false 57 | config.setFrom("$projectDir/../detekt/detekt.yml") 58 | } 59 | tasks.withType().configureEach { 60 | reports { 61 | html.required.set(true) 62 | md.required.set(true) 63 | xml.required.set(false) 64 | txt.required.set(false) 65 | sarif.required.set(false) 66 | } 67 | } 68 | 69 | tasks.withType().configureEach { 70 | outputDirectory.set(file("$rootDir/docs/dokka/ktor-swagger-ui")) 71 | } 72 | 73 | mavenPublishing { 74 | val projectGroupId: String by project 75 | val projectVersion: String by project 76 | val projectBaseScmUrl: String by project 77 | val projectBaseScmConnection: String by project 78 | val projectLicenseName: String by project 79 | val projectLicenseUrl: String by project 80 | val projectDeveloperName: String by project 81 | val projectDeveloperUrl: String by project 82 | 83 | configure(KotlinJvm(JavadocJar.Dokka("dokkaHtml"), true)) 84 | publishToMavenCentral(SonatypeHost.S01) 85 | signAllPublications() 86 | coordinates(projectGroupId, "ktor-swagger-ui", projectVersion) 87 | pom { 88 | name.set("Ktor Swagger-UI") 89 | description.set("Ktor plugin to provide a Swagger-UI") 90 | url.set(projectBaseScmUrl + "ktor-swagger-ui") 91 | licenses { 92 | license { 93 | name.set(projectLicenseName) 94 | url.set(projectLicenseUrl) 95 | distribution.set(projectLicenseUrl) 96 | } 97 | } 98 | scm { 99 | url.set(projectBaseScmUrl + "ktor-swagger-ui") 100 | connection.set(projectBaseScmConnection + "ktor-swagger-ui.git") 101 | } 102 | developers { 103 | developer { 104 | id.set(projectDeveloperName) 105 | name.set(projectDeveloperName) 106 | url.set(projectDeveloperUrl) 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/PropertyBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui 2 | 3 | 4 | 5 | internal class PropertyBuilder { 6 | 7 | data class Raw(val value: String) 8 | 9 | private val entries = mutableMapOf() 10 | 11 | operator fun set(key: String, value: Any?) { 12 | entries[key] = value 13 | } 14 | 15 | 16 | /** 17 | * @param linePrefix a string to add before each line 18 | * @param prefixFirst whether to add the prefix to the first line 19 | * @param separator the separator string to join each line with 20 | * @param nullBehavior how to handle null values. Valid values are "include", "remove", "to_undefined" 21 | */ 22 | fun render(linePrefix: String, prefixFirst: Boolean, separator: String, nullBehavior: String): String { 23 | return entries 24 | .mapValues { (_, value) -> 25 | when (value) { 26 | null -> when(nullBehavior) { 27 | "include" -> "null" 28 | "remove" -> null 29 | "to_undefined" -> "undefined" 30 | else -> throw IllegalArgumentException("Invalid value for null behaviour: '$nullBehavior'") 31 | } 32 | is Raw -> value.value 33 | is Number -> value.toString() 34 | is Boolean -> value.toString() 35 | is String -> "'$value'" 36 | else -> "'$value'" 37 | } 38 | } 39 | .filterValues { it != null } 40 | .mapValues { (_, value) -> value!! } 41 | .map { (key, value) -> "$key: $value" } 42 | .mapIndexed { index, line -> 43 | if(!prefixFirst && index == 0) { 44 | line 45 | } else { 46 | linePrefix + line 47 | } 48 | } 49 | .joinToString(separator) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/ResourceContent.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui 2 | 3 | import io.ktor.http.ContentType 4 | import io.ktor.http.content.OutgoingContent 5 | import java.net.URL 6 | 7 | internal class ResourceContent(private val resource: URL) : OutgoingContent.ByteArrayContent() { 8 | 9 | private val contentTypes = mapOf( 10 | "html" to ContentType.Text.Html, 11 | "css" to ContentType.Text.CSS, 12 | "js" to ContentType.Application.JavaScript, 13 | "json" to ContentType.Application.Json, 14 | "png" to ContentType.Image.PNG 15 | ) 16 | 17 | private val bytes by lazy { resource.readBytes() } 18 | 19 | override val contentType: ContentType? by lazy { 20 | val extension = resource.file.substring(resource.file.lastIndexOf('.') + 1) 21 | contentTypes[extension] ?: ContentType.Text.Html 22 | } 23 | 24 | override val contentLength: Long? by lazy { 25 | bytes.size.toLong() 26 | } 27 | 28 | override fun bytes(): ByteArray = bytes 29 | 30 | override fun toString() = "ResourceContent \"$resource\"" 31 | } 32 | -------------------------------------------------------------------------------- /ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIRouteSelector.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui 2 | 3 | import io.ktor.server.routing.Route 4 | import io.ktor.server.routing.RouteSelector 5 | import io.ktor.server.routing.RouteSelectorEvaluation 6 | import io.ktor.server.routing.RoutingResolveContext 7 | 8 | internal class SwaggerUIRouteSelector : RouteSelector() { 9 | override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int) = RouteSelectorEvaluation.Transparent 10 | } 11 | 12 | internal fun Route.markedSwaggerUI(build: Route.() -> Unit): Route { 13 | return createChild(SwaggerUIRouteSelector()).also(build) 14 | } 15 | -------------------------------------------------------------------------------- /ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/config/OperationsSort.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui.config 2 | 3 | /** 4 | * Determines the order to sort the operations in the swagger-ui. 5 | */ 6 | enum class OperationsSort(val value: String) { 7 | /** 8 | * The order returned by the server unchanged 9 | */ 10 | NONE("undefined"), 11 | /** 12 | * sort by paths alphanumerically 13 | */ 14 | ALPHANUMERICALLY("alpha"), 15 | /** 16 | * sort by HTTP method 17 | */ 18 | HTTP_METHOD("method") 19 | } 20 | -------------------------------------------------------------------------------- /ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/config/SwaggerUISyntaxHighlight.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui.config 2 | 3 | /** 4 | * The theme to use for syntax highlighting code-blocks in swagger-ui. 5 | */ 6 | enum class SwaggerUISyntaxHighlight(val value: String) { 7 | DISABLED("disabled"), 8 | AGATE("agate"), 9 | ARTA("arta"), 10 | MONOKAI("monokai"), 11 | NORD("nord"), 12 | OBSIDIAN("obsidian"), 13 | TOMORROW_NIGHT("tomorrow-night"), 14 | IDEA("idea") 15 | } 16 | -------------------------------------------------------------------------------- /ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/config/TagSort.kt: -------------------------------------------------------------------------------- 1 | package io.github.smiley4.ktorswaggerui.config 2 | 3 | /** 4 | * Determines the order to sort the tags in the swagger-ui. 5 | */ 6 | enum class TagSort(val value: String) { 7 | /** 8 | * The order returned by the server unchanged 9 | */ 10 | NONE("undefined"), 11 | /** 12 | * sort by paths alphanumerically 13 | */ 14 | ALPHANUMERICALLY("alpha"), 15 | } 16 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ktor-openapi" 2 | 3 | include("ktor-openapi") 4 | include("ktor-swagger-ui") 5 | include("ktor-redoc") 6 | 7 | include("examples") 8 | 9 | pluginManagement { 10 | repositories { 11 | mavenCentral() 12 | gradlePluginPortal() 13 | } 14 | } 15 | 16 | plugins { 17 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 18 | } --------------------------------------------------------------------------------