Compare commits

..

29 Commits

Author SHA1 Message Date
d97717f8f8 fix: bug in doc publishing pipeline 2022-01-23 14:28:48 -05:00
3c585c06a3 feat: kompendium 2.0.0 release! 2022-01-23 14:14:44 -05:00
7bfd168d74 chore(deps): update endbug/add-and-commit action to v8 (#158)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-21 17:00:22 -05:00
19298f4deb chore(deps): update plugin io.bkbn.sourdough.root to v0.6.0 (#161)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-21 14:46:51 +00:00
54bdf107e2 chore(deps): update plugin io.bkbn.sourdough.library.jvm to v0.6.0 (#160)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-21 12:35:30 +00:00
721302d651 chore(deps): update plugin io.bkbn.sourdough.application.jvm to v0.6.0 (#159)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-21 11:09:16 +00:00
3ffda43a52 chore(deps): update dependency org.webjars:swagger-ui to v4.2.1 (#157)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-20 10:34:17 +00:00
ae2a1b578a chore: version bumps and cleanup (#156) 2022-01-17 03:22:33 +00:00
147c7e7fb0 chore(deps): update kotestversion to v5.1.0 (#155)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-16 19:02:58 -05:00
906b329c2e chore(deps): update dependency org.slf4j:slf4j-simple to v1.7.33 (#153)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-13 19:03:18 +00:00
1bf81cfd82 chore(deps): update dependency org.slf4j:slf4j-api to v1.7.33 (#152)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-13 17:25:57 +00:00
c9f173d6b0 chore(deps): update plugin io.bkbn.sourdough.root to v0.5.5 (#151)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-13 13:19:33 +00:00
c43fafae1b chore(deps): update plugin io.bkbn.sourdough.library.jvm to v0.5.5 (#150)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-13 08:17:21 -05:00
3b2fa72d26 chore(deps): update plugin io.bkbn.sourdough.application.jvm to v0.5.5 (#149)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-13 12:25:52 +00:00
91d4ec10b7 chore(deps): update dependency org.webjars:swagger-ui to v4.1.3-1 (#148)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-13 10:15:55 +00:00
aa1b898b22 fix: adding signing plugin (#147) 2022-01-12 11:19:19 -05:00
aa21c1219b fix: bug in testing suite + lots of documentation updates 2022-01-12 09:15:27 -05:00
bc380077fb chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.6.10 (#143) 2022-01-09 13:09:51 +00:00
fc9929e9cc chore(deps): update plugin io.bkbn.sourdough.root to v0.3.3 (#142) 2022-01-09 08:05:52 -05:00
a26ad72b67 chore(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.3.2 (#141)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-09 11:24:03 +00:00
38a70e4979 chore(deps): update dependency org.apache.logging.log4j:log4j-core to v2.17.1 (#140)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-09 09:47:01 +00:00
4a1425b73b chore(deps): update dependency org.apache.logging.log4j:log4j-api to v2.17.1 (#138) 2022-01-09 03:13:12 +00:00
01c118373e chore(deps): update dependency gradle to v7.3.3 (#137) 2022-01-08 22:09:03 -05:00
7535d67661 chore(deps): add renovate.json 2022-01-09 02:58:31 +00:00
eb369dcdc8 fix: locations inheritance (#135) 2022-01-07 08:46:20 -05:00
da104d0a63 feat: Multi Serialization Support (#134) 2022-01-04 02:05:30 +00:00
c6ed261fe4 feat: enable creation of explicit parameter examples (#133) 2022-01-03 10:34:02 -05:00
012db5ad26 feat: added head, patch, and options methods (#132) 2022-01-03 14:32:55 +00:00
f02f7ad211 hotfix: hopefully fix bug in release pipeline 2022-01-02 23:34:23 -05:00
99 changed files with 1581 additions and 556 deletions

View File

@ -15,9 +15,6 @@ jobs:
with: with:
gradle-version: wrapper gradle-version: wrapper
arguments: detekt arguments: detekt
properties: |
org.gradle.vfs.watch=false
org.gradle.vfs.verbose=false
unit: unit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -32,6 +29,3 @@ jobs:
with: with:
gradle-version: wrapper gradle-version: wrapper
arguments: test koverCollectReports arguments: test koverCollectReports
properties: |
org.gradle.vfs.watch=false
org.gradle.vfs.verbose=false

View File

@ -2,6 +2,8 @@ name: Publish to GitHub Packages
on: on:
push: push:
branches: [ main ] branches: [ main ]
paths-ignore:
- docs/**
env: env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }} ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
@ -19,8 +21,5 @@ jobs:
with: with:
gradle-version: wrapper gradle-version: wrapper
arguments: publishAllPublicationsToGithubPackagesRepository arguments: publishAllPublicationsToGithubPackagesRepository
properties: |
org.gradle.vfs.watch=false
org.gradle.vfs.verbose=false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,4 +1,4 @@
name: Publish to GitHub Packages name: Publish Release
on: on:
release: release:
types: types:
@ -8,25 +8,6 @@ env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }} ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
jobs: jobs:
publish-to-github:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '17'
- name: Publish to GitHub Packages
uses: burrunan/gradle-cache-action@v1
with:
gradle-version: wrapper
arguments: publishAllPublicationsToGithubPackagesRepository
properties: |
release=true
org.gradle.vfs.watch=false
org.gradle.vfs.verbose=false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-to-nexus: publish-to-nexus:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -35,12 +16,38 @@ jobs:
with: with:
distribution: 'adopt' distribution: 'adopt'
java-version: '17' java-version: '17'
- name: Publlish to GithubPackages - name: Publlish to Maven Central
uses: burrunan/gradle-cache-action@v1 uses: burrunan/gradle-cache-action@v1
with: with:
gradle-version: wrapper gradle-version: wrapper
arguments: publishToSonatype closeAndReleaseSonatypeStagingRepository arguments: publishToSonatype closeAndReleaseSonatypeStagingRepository
properties: | properties: |
release=true release=true
org.gradle.vfs.watch=false env:
org.gradle.vfs.verbose=false ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USER }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
build-documentation:
runs-on: ubuntu-latest
needs:
- publish-to-nexus
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '17'
- name: Build New Documentation
uses: burrunan/gradle-cache-action@v1
with:
gradle-version: wrapper
arguments: dokkaHtmlMultiModule
properties: |
release=true
- name: Push New Documentation
uses: EndBug/add-and-commit@v8
with:
default_author: github_actions
new_branch: main
message: 'doc: Added Latest Documentation ✨'

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
.gradle .gradle
build build
.idea .idea
dokka
wiki

View File

@ -1,6 +1,7 @@
# Changelog # Changelog
## Unreleased ## Unreleased
### Added ### Added
### Changed ### Changed
@ -11,8 +12,24 @@
## Released ## Released
## [2.0.0-alpha] - January 2nd, 2021 ## [2.0.1] - January 23rd, 2022
### Change
- Fix bug in documentation publishing pipeline
## [2.0.0] - January 23rd, 2022
Major Release 🎉 As we head towards the Ktor 2 release, this library will be kept compatible with Ktor 1. A future
Kompendium 2 repository will be created soon, porting much of the changes you see here, with some awesome Ktor 2 twists
😉
### Added ### Added
- Support for HTTP Patch, Head, and Options methods
- Support for including parameter examples via `MethodInfo`
- Dokka Pipeline Generation
- GitHub Pages integration
- Sourdough Gradle updates
- Support for OAuth authentication - Support for OAuth authentication
- Gradle Toolchain feature to ensure match between local JDK and compile target - Gradle Toolchain feature to ensure match between local JDK and compile target
- Dokka integration - Dokka integration
@ -22,6 +39,10 @@
- Ability to document expected unstructured data - Ability to document expected unstructured data
### Changed ### Changed
- Kompendium now leverages the chosen API serializer. Supports Jackson, Gson and Kotlinx Serialization
- Fixed bug where overridden field names were not reflected in serialized object and required array
- Fixed bug where Ktor Location parents were not being scanned for parameters
- `$ref` types are no longer generated, instead all objects are defined explicitly - `$ref` types are no longer generated, instead all objects are defined explicitly
- All OpenAPI domain models moved to a separate module `kompendium-oas` - All OpenAPI domain models moved to a separate module `kompendium-oas`
- Moved all files in `kompendium-core` into `io.bkbn.kompendium.core` package from `io.bkbn.kompendium` - Moved all files in `kompendium-core` into `io.bkbn.kompendium.core` package from `io.bkbn.kompendium`
@ -29,7 +50,8 @@
- Gradle build logic offloaded to Sourdough Plugin - Gradle build logic offloaded to Sourdough Plugin
- Minimum supported Java version is now 11 - Minimum supported Java version is now 11
- Bumped Kotlin to 1.6 - Bumped Kotlin to 1.6
- Annotations now live in a separate module. (Should not impact end users as module is imported as api dependency by core). - Annotations now live in a separate module. (Should not impact end users as module is imported as api dependency by
core).
- Kotest as the testing framework of choice - Kotest as the testing framework of choice
- Path calculation removed in favor of built-in route toString - Path calculation removed in favor of built-in route toString
- Ktor to 1.6.7 - Ktor to 1.6.7
@ -55,11 +77,15 @@
- Dropped ASDF tool manifest - Dropped ASDF tool manifest
## [1.11.1] - November 25th, 2021 ## [1.11.1] - November 25th, 2021
### Added ### Added
- Documentation showing how to add header names using Kotlin backtick convention - Documentation showing how to add header names using Kotlin backtick convention
## [1.11.0] - November 25th, 2021 ## [1.11.0] - November 25th, 2021
### Added ### Added
- Support for Ktor Location Plugin - Support for Ktor Location Plugin
## [1.10.0] - November 25th, 2021 ## [1.10.0] - November 25th, 2021
@ -89,7 +115,6 @@
- ByteArray added to the set of default types - ByteArray added to the set of default types
## [1.8.1] - October 4th, 2021 ## [1.8.1] - October 4th, 2021
### Added ### Added
@ -258,7 +283,8 @@ This is just to get my repo back to normal now that I have confirmed sonatype pu
### Added ### Added
- Added an explicit `PathCalculator` interface to allow for easier handling of routes external to the core set of Ktor route selectors. - Added an explicit `PathCalculator` interface to allow for easier handling of routes external to the core set of Ktor
route selectors.
## [0.5.1] - April 19th, 2021 ## [0.5.1] - April 19th, 2021
@ -349,12 +375,13 @@ This is just to get my repo back to normal now that I have confirmed sonatype pu
### Added ### Added
- Beginning of an implementation. Currently, able to generate a rough outline of the API at runtime, along with generating - Beginning of an implementation. Currently, able to generate a rough outline of the API at runtime, along with
full data classes represented by JSON Schema. generating full data classes represented by JSON Schema.
## [0.0.1] - April 11th, 2021 ## [0.0.1] - April 11th, 2021
### Added ### Added
- Added _most_ of the data classes necessary for generating an [Open API Spec](https://swagger.io/specification) - Added _most_ of the data classes necessary for generating an [Open API Spec](https://swagger.io/specification)
- Added playground to allow users to tinker with a live Ktor api in conjunction with development - Added playground to allow users to tinker with a live Ktor api in conjunction with development
- Added all standard OSS files - Added all standard OSS files

41
Project.md Normal file
View File

@ -0,0 +1,41 @@
# Kompendium
Welcome to Kompendium, the straight-forward, minimally-invasive OpenAPI generator for Ktor.
## How to install
Kompendium publishes all releases to Maven Central. As such, using the release versions of `Kompendium` is as simple as
declaring it as an implementation dependency in your `build.gradle.kts`
```kotlin
repositories {
mavenCentral()
}
dependencies {
implementation("io.bkbn:kompendium-core:latest.release")
}
```
In addition to publishing releases to Maven Central, a snapshot version gets published to GitHub Packages on every merge
to `main`. These can be consumed by adding the repository to your gradle build file. Instructions can be
found [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package)
## Setting up the Kompendium Plugin
Kompendium is instantiated as a Ktor Feature/Plugin. It can be added to your API as follows
```kotlin
private fun Application.mainModule() {
// Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) {
spec = OpenApiSpec(/*..*/)
}
// ...
}
```
## Notarization
The concept of notarizing routes / exceptions / etc. is central to Kompendium. More details on _how_ to notarize your
API can be found in the kompendium-core module.

View File

@ -31,15 +31,13 @@ dependencies {
} }
``` ```
The last two dependencies are optional.
In addition to publishing releases to Maven Central, a snapshot version gets published to GitHub Packages on every merge In addition to publishing releases to Maven Central, a snapshot version gets published to GitHub Packages on every merge
to `main`. These can be consumed by adding the repository to your gradle build file. Instructions can be to `main`. These can be consumed by adding the repository to your gradle build file. Instructions can be
found [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package) found [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package)
# Library Details # Library Details
Details on how to use Kompendium can be found in the Wiki (WIP, this is new for V2 and will be fleshed out prior to release) Forthcoming, more details on V2 will be published soon :)
## Local Development ## Local Development

View File

@ -1,14 +1,13 @@
import io.bkbn.sourdough.gradle.core.extension.SourdoughLibraryExtension
plugins { plugins {
id("io.bkbn.sourdough.root") version "0.3.0" kotlin("jvm") version "1.6.10" apply false
kotlin("plugin.serialization") version "1.6.10" apply false
id("io.bkbn.sourdough.library.jvm") version "0.6.0" apply false
id("io.bkbn.sourdough.application.jvm") version "0.6.0" apply false
id("io.bkbn.sourdough.root") version "0.6.0"
id("com.github.jakemarsden.git-hooks") version "0.0.2" id("com.github.jakemarsden.git-hooks") version "0.0.2"
} id("org.jetbrains.dokka") version "1.6.10"
id("org.jetbrains.kotlinx.kover") version "0.5.0-RC"
sourdough { id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
toolChainJavaVersion.set(JavaLanguageVersion.of(JavaVersion.VERSION_17.majorVersion))
jvmTarget.set(JavaVersion.VERSION_11.majorVersion)
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
} }
gitHooks { gitHooks {
@ -33,17 +32,15 @@ allprojects {
} }
subprojects { subprojects {
apply(plugin = "io.bkbn.sourdough.library") plugins.withType(io.bkbn.sourdough.gradle.library.jvm.LibraryJvmPlugin::class) {
extensions.configure(io.bkbn.sourdough.gradle.library.jvm.LibraryJvmExtension::class) {
configure<SourdoughLibraryExtension> {
githubOrg.set("bkbnio") githubOrg.set("bkbnio")
githubRepo.set("kompendium") githubRepo.set("kompendium")
libraryName.set("Kompendium")
libraryDescription.set("A minimally invasive OpenAPI spec generator for Ktor")
licenseName.set("MIT License") licenseName.set("MIT License")
licenseUrl.set("https://mit-license.org") licenseUrl.set("https://mit-license.org")
developerId.set("bkbnio") developerId.set("unredundant")
developerName.set("Ryan Brink") developerName.set("Ryan Brink")
developerEmail.set("admin@bkbn.io") developerEmail.set("admin@bkbn.io")
} }
}
} }

View File

@ -1,5 +1,5 @@
# Kompendium # Kompendium
project.version=2.0.0-alpha project.version=2.0.1
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # Gradle
@ -9,4 +9,4 @@ org.gradle.jvmargs=-Xmx2000m
# Dependencies # Dependencies
ktorVersion=1.6.7 ktorVersion=1.6.7
kotestVersion=5.0.3 kotestVersion=5.1.0

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -7,3 +7,7 @@ It is separated from core predominantly to allow for potential future integratio
# Package io.bkbn.kompendium.annotations # Package io.bkbn.kompendium.annotations
Contains all annotations used by Kompendium Contains all annotations used by Kompendium
# Package io.bkbn.kompendium.annotations.constraint
Annotations that place bespoke constraints on individual fields of your API schemas.

View File

@ -1,3 +1,23 @@
plugins { plugins {
id("io.bkbn.sourdough.library") kotlin("jvm")
id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdough {
libraryName.set("Kompendium Annotations")
libraryDescription.set("A set of annotations used by Kompendium to generate OpenAPI Specifications")
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
} }

View File

@ -5,5 +5,9 @@ their API authentication with minimal modifications to their existing configurat
# Package io.bkbn.kompendium.auth # Package io.bkbn.kompendium.auth
Base package that is responsible for setting up required authentication route handlers along with exposing Base package that is responsible for setting up required authentication route handlers along with exposing wrapper
wrapper methods for each ktor-auth authentication mechanism. methods for each ktor-auth authentication mechanism.
# Package io.bkbn.kompendium.auth
Houses the available security configurations. At the moment, `Basic`, `JWT`, `ApiKey`, and `OAuth` are supported

View File

@ -1,7 +1,18 @@
plugins { plugins {
id("io.bkbn.sourdough.library") kotlin("jvm")
id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
} }
sourdough {
libraryName.set("Kompendium Authentication")
libraryDescription.set("Kompendium library to pair with Ktor Auth to provide authorization info to OpenAPI")
}
dependencies { dependencies {
// IMPLEMENTATION // IMPLEMENTATION
@ -16,3 +27,11 @@ dependencies {
testImplementation(testFixtures(projects.kompendiumCore)) testImplementation(testFixtures(projects.kompendiumCore))
} }
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -6,4 +6,95 @@ It is also the only mandatory client-facing module for a basic setup.
# Package io.bkbn.kompendium.core # Package io.bkbn.kompendium.core
The root package contains several objects that power Kompendium The root package contains several objects that power Kompendium, including the Kompendium Ktor Plugin, route
notarization methods, and the reflection engine that analyzes method info type parameters.
## Plugin
The Kompendium plugin is an extremely light-weight plugin, with only a couple areas of customization.
### Serialization
Kompendium relies on your API to provide a properly-configured `ContentNegotiator` in order to convert the `OpenApiSpec`
into JSON. The advantage to this approach is that all of your data classes will be serialized precisely how you define.
The downside is that issues could exist in serialization frameworks that have not been tested. At the moment, Jackson,
Gson and KotlinX serialization have all been tested. If you run into any serialization issues, particularly with a
serializer not listed above, please open an issue on GitHub 🙏
## Notarization
Central to Kompendium is the concept of notarization.
Notarizing a route is the mechanism by which Kompendium analyzes your route types, along with provided metadata, and
converts to the expected OpenAPI format.
Before jumping into notarization, lets first look at a standard Ktor route
```kotlin
routing {
get {
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
}
}
```
Now, let's compare this to the same functionality, but notarized using Kompendium
```kotlin
routing {
notarizedGet(simpleGetExample) {
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
}
}
```
Pretty simple huh. But hold on... what is this `simpleGetExample`? How can I know that it is so "simple". Let's take a
look
```kotlin
val simpleGetExample = GetInfo<Unit, BasicResponse>(
summary = "Simple, Documented GET Request",
description = "This is to showcase just how easy it is to document your Ktor API!",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "This means everything went as expected!",
examples = mapOf("demo" to BasicResponse(c = "52c099d7-8642-46cc-b34e-22f39b923cf4"))
),
tags = setOf("Simple")
)
```
See, not so bad 😄 `GetInfo<*,*>` is an implementation of `MethodInfo<TParam, TResp>`, a sealed interface designed to
encapsulate all the metadata required for documenting an API route. Kompendium leverages this data, along with the
provided type parameters `TParam` and `TResp` to construct the full OpenAPI Specification for your route.
Additionally, just as a backup, each notarization method includes a "post-processing' hook that will allow you to have
final say in the generated route info prior to being attached to the spec. This can be accessed via the optional
parameter
```kotlin
routing {
notarizedGet(simpleGetExample, postProcess = { spec -> spec }) {
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
}
}
```
This should only be used in _extremely_ rare scenarios, but it is nice to know it is there if you need it.
# Package io.bkbn.kompendium.core.metadata
Houses all interfaces and types related to describing route metadata.
# Package io.bkbn.kompendium.core.parser
Responsible for the parse of method information. Base implementation is an interface to support extensibility as shown
in the `kompendium-locations` module.
# Package io.bkbn.kompendium.core.routes
Houses any routes provided by the core module. At the moment the only supported route is to enable ReDoc support.
# Package io.bkbn.kompendium.core.util
Collection of utility functions used by Kompendium

View File

@ -1,21 +1,34 @@
plugins { plugins {
id("io.bkbn.sourdough.library") kotlin("jvm")
`java-test-fixtures` id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
id("java-test-fixtures")
}
sourdough {
libraryName.set("Kompendium Core")
libraryDescription.set("Core functionality for the Kompendium library")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
} }
dependencies { dependencies {
// VERSIONS
val ktorVersion: String by project
val kotestVersion: String by project
// IMPLEMENTATION // IMPLEMENTATION
api(projects.kompendiumOas) api(projects.kompendiumOas)
api(projects.kompendiumAnnotations) api(projects.kompendiumAnnotations)
val ktorVersion: String by project
val kotestVersion: String by project
implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-html-builder", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-html-builder", version = ktorVersion)
implementation(group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version = "2.13.0")
// TEST FIXTURES // TEST FIXTURES
testFixturesApi(group = "io.kotest", name = "kotest-runner-junit5-jvm", version = kotestVersion) testFixturesApi(group = "io.kotest", name = "kotest-runner-junit5-jvm", version = kotestVersion)
@ -29,5 +42,13 @@ dependencies {
testFixturesApi(group = "io.ktor", name = "ktor-jackson", version = ktorVersion) testFixturesApi(group = "io.ktor", name = "ktor-jackson", version = ktorVersion)
testFixturesApi(group = "io.ktor", name = "ktor-serialization", version = ktorVersion) testFixturesApi(group = "io.ktor", name = "ktor-serialization", version = ktorVersion)
testFixturesApi(group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.3.1") testFixturesApi(group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.3.2")
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
} }

View File

@ -1,8 +1,5 @@
package io.bkbn.kompendium.core package io.bkbn.kompendium.core
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.metadata.SchemaMap import io.bkbn.kompendium.core.metadata.SchemaMap
import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.schema.TypedSchema import io.bkbn.kompendium.oas.schema.TypedSchema
@ -12,7 +9,7 @@ import io.ktor.application.ApplicationFeature
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.request.path import io.ktor.request.path
import io.ktor.response.respondText import io.ktor.response.respond
import io.ktor.util.AttributeKey import io.ktor.util.AttributeKey
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -28,13 +25,6 @@ class Kompendium(val config: Configuration) {
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) { fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
cache = cache.plus(clazz.simpleName!! to schema) cache = cache.plus(clazz.simpleName!! to schema)
} }
// TODO Add tests for this!!
var om: ObjectMapper = ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.enable(SerializationFeature.INDENT_OUTPUT)
fun specToJson(): String = om.writeValueAsString(spec)
} }
companion object Feature : ApplicationFeature<Application, Configuration, Kompendium> { companion object Feature : ApplicationFeature<Application, Configuration, Kompendium> {
@ -44,8 +34,7 @@ class Kompendium(val config: Configuration) {
pipeline.intercept(ApplicationCallPipeline.Call) { pipeline.intercept(ApplicationCallPipeline.Call) {
if (call.request.path() == configuration.specRoute) { if (call.request.path() == configuration.specRoute) {
call.respondText { configuration.specToJson() } call.respond(HttpStatusCode.OK, configuration.spec)
call.response.status(HttpStatusCode.OK)
} }
} }

View File

@ -199,8 +199,13 @@ object Kontent {
logger.debug("$slug contains $fieldMap") logger.debug("$slug contains $fieldMap")
var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap)) var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList() val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
// todo de-dup this logic
if (requiredParams.isNotEmpty()) { if (requiredParams.isNotEmpty()) {
schema = schema.copy(required = requiredParams.map { it.name!! }) schema = schema.copy(required = requiredParams.map { param ->
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
?.let { field -> field.name.ifBlank { param.name!! } }
?: param.name!!
})
} }
logger.debug("$slug schema: $schema") logger.debug("$slug schema: $schema")
newCache.plus(slug to schema) newCache.plus(slug to schema)
@ -335,8 +340,13 @@ object Kontent {
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList() val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
var schema = this var schema = this
// todo dedup this
if (requiredParams.isNotEmpty()) { if (requiredParams.isNotEmpty()) {
schema = schema.copy(required = requiredParams.map { it.name!! }) schema = schema.copy(required = requiredParams.map { param ->
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
?.let { field -> field.name.ifBlank { param.name!! } }
?: param.name!!
})
} }
if (prop.returnType.isMarkedNullable) { if (prop.returnType.isMarkedNullable) {

View File

@ -2,11 +2,15 @@ package io.bkbn.kompendium.core
import io.bkbn.kompendium.annotations.Param import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
import io.bkbn.kompendium.core.MethodParser.parseMethodInfo
import io.bkbn.kompendium.core.metadata.method.DeleteInfo import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.HeadInfo
import io.bkbn.kompendium.core.metadata.method.OptionsInfo
import io.bkbn.kompendium.core.metadata.method.PatchInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.core.parser.DefaultMethodParser.calculateRoutePath
import io.bkbn.kompendium.core.parser.DefaultMethodParser.parseMethodInfo
import io.bkbn.kompendium.oas.path.Path import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation import io.bkbn.kompendium.oas.path.PathOperation
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
@ -66,7 +70,7 @@ object Notarized {
} }
/** /**
* Notarization for an HTTP Delete request * Notarization for an HTTP PUT request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param] * @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TReq Class detailing the expected API request body * @param TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response * @param TResp Class detailing the expected API response
@ -87,7 +91,28 @@ object Notarized {
} }
/** /**
* Notarization for an HTTP POST request * Notarization for an HTTP PATCH request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPatch(
info: PatchInfo<TParam, TReq, TResp>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
): Route = methodNotarizationPreFlight<TParam, TReq, TResp> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.patch = postProcess(baseInfo)
return method(HttpMethod.Patch) { handle(body) }
}
/**
* Notarization for an HTTP DELETE request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param] * @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TResp Class detailing the expected API response * @param TResp Class detailing the expected API response
* @param info Route metadata * @param info Route metadata
@ -107,8 +132,41 @@ object Notarized {
} }
/** /**
* Uses the built-in Ktor route path [Route.toString] but cuts out any meta route such as authentication... anything * Notarization for an HTTP HEAD request
* that matches the RegEx pattern `/\\(.+\\)` * @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/ */
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "") inline fun <reified TParam : Any> Route.notarizedHead(
info: HeadInfo<TParam>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = methodNotarizationPreFlight<TParam, Unit, Unit> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.head = postProcess(baseInfo)
return method(HttpMethod.Head) { handle(body) }
}
/**
* Notarization for an HTTP OPTION request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TResp Class detailing the expected API response
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedOptions(
info: OptionsInfo<TParam, TResp>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.options = postProcess(baseInfo)
return method(HttpMethod.Options) { handle(body) }
}
} }

View File

@ -0,0 +1,3 @@
package io.bkbn.kompendium.core.metadata
data class ParameterExample(val parameterName: String, val exampleName: String, val exampleValue: Any)

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
data class DeleteInfo<TParam, TResp>( data class DeleteInfo<TParam, TResp>(
@ -11,6 +12,6 @@ data class DeleteInfo<TParam, TResp>(
override val deprecated: Boolean = false, override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(), override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(), override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(), override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null override val operationId: String? = null
) : MethodInfo<TParam, TResp> ) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
data class GetInfo<TParam, TResp>( data class GetInfo<TParam, TResp>(
@ -11,6 +12,6 @@ data class GetInfo<TParam, TResp>(
override val deprecated: Boolean = false, override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(), override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(), override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(), override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null override val operationId: String? = null
) : MethodInfo<TParam, TResp> ) : MethodInfo<TParam, TResp>

View File

@ -0,0 +1,17 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class HeadInfo<TParam>(
override val responseInfo: ResponseInfo<Unit>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, Unit>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
sealed interface MethodInfo<TParam, TResp> { sealed interface MethodInfo<TParam, TResp> {
@ -16,9 +17,8 @@ sealed interface MethodInfo<TParam, TResp> {
val canThrow: Set<ExceptionInfo<*>> val canThrow: Set<ExceptionInfo<*>>
get() = emptySet() get() = emptySet()
val responseInfo: ResponseInfo<TResp> val responseInfo: ResponseInfo<TResp>
// TODO Is this even used anywhere? val parameterExamples: Set<ParameterExample>
val parameterExamples: Map<String, TParam> get() = emptySet()
get() = emptyMap()
val operationId: String? val operationId: String?
get() = null get() = null
} }

View File

@ -0,0 +1,17 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class OptionsInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -0,0 +1,19 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class PatchInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>?,
override val responseInfo: ResponseInfo<TResp>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
@ -13,6 +14,6 @@ data class PostInfo<TParam, TReq, TResp>(
override val deprecated: Boolean = false, override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(), override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(), override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(), override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null override val operationId: String? = null
) : MethodInfo<TParam, TResp> ) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
@ -13,6 +14,6 @@ data class PutInfo<TParam, TReq, TResp>(
override val deprecated: Boolean = false, override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(), override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(), override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(), override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null override val operationId: String? = null
) : MethodInfo<TParam, TResp> ) : MethodInfo<TParam, TResp>

View File

@ -0,0 +1,3 @@
package io.bkbn.kompendium.core.parser
object DefaultMethodParser : IMethodParser

View File

@ -1,8 +1,10 @@
package io.bkbn.kompendium.core package io.bkbn.kompendium.core.parser
import io.bkbn.kompendium.annotations.Param import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.Kontent.generateKontent import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Kontent
import io.bkbn.kompendium.core.metadata.ExceptionInfo import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.MethodInfo import io.bkbn.kompendium.core.metadata.method.MethodInfo
@ -18,22 +20,20 @@ import io.bkbn.kompendium.oas.payload.Request
import io.bkbn.kompendium.oas.payload.Response import io.bkbn.kompendium.oas.payload.Response
import io.bkbn.kompendium.oas.schema.AnyOfSchema import io.bkbn.kompendium.oas.schema.AnyOfSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.ktor.routing.Route
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KParameter import kotlin.reflect.KParameter
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor import kotlin.reflect.full.primaryConstructor
import java.util.Locale import java.util.Locale
import java.util.UUID import java.util.UUID
/** interface IMethodParser {
* The MethodParser is responsible for converting route metadata and types into an OpenAPI compatible data class.
*/
object MethodParser {
/** /**
* Generates the OpenAPI Path spec from provided metadata * Generates the OpenAPI Path spec from provided metadata
* @param info implementation of the [MethodInfo] sealed class * @param info implementation of the [MethodInfo] sealed class
@ -54,7 +54,7 @@ object MethodParser {
operationId = info.operationId, operationId = info.operationId,
tags = info.tags, tags = info.tags,
deprecated = info.deprecated, deprecated = info.deprecated,
parameters = paramType.toParameterSpec(feature), parameters = paramType.toParameterSpec(info, feature),
responses = parseResponse(responseType, info.responseInfo, feature).plus(parseExceptions(info.canThrow, feature)), responses = parseResponse(responseType, info.responseInfo, feature).plus(parseExceptions(info.canThrow, feature)),
requestBody = when (info) { requestBody = when (info) {
is PutInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo, feature) is PutInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo, feature)
@ -67,17 +67,17 @@ object MethodParser {
) else null ) else null
) )
private fun parseResponse( fun parseResponse(
responseType: KType, responseType: KType,
responseInfo: ResponseInfo<*>?, responseInfo: ResponseInfo<*>?,
feature: Kompendium feature: Kompendium
): Map<Int, Response<*>> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty() ): Map<Int, Response> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty()
private fun parseExceptions( fun parseExceptions(
exceptionInfo: Set<ExceptionInfo<*>>, exceptionInfo: Set<ExceptionInfo<*>>,
feature: Kompendium, feature: Kompendium,
): Map<Int, Response<*>> = exceptionInfo.associate { info -> ): Map<Int, Response> = exceptionInfo.associate { info ->
feature.config.cache = generateKontent(info.responseType, feature.config.cache) feature.config.cache = Kontent.generateKontent(info.responseType, feature.config.cache)
val response = Response( val response = Response(
description = info.description, description = info.description,
content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples) content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples)
@ -91,13 +91,14 @@ object MethodParser {
* @param requestInfo request metadata * @param requestInfo request metadata
* @return Will return a generated [Request] if requestInfo is not null * @return Will return a generated [Request] if requestInfo is not null
*/ */
private fun KType.toRequestSpec(requestInfo: RequestInfo<*>?, feature: Kompendium): Request<*>? = fun KType.toRequestSpec(requestInfo: RequestInfo<*>?, feature: Kompendium): Request? =
when (requestInfo) { when (requestInfo) {
null -> null null -> null
else -> { else -> {
Request( Request(
description = requestInfo.description, description = requestInfo.description,
content = feature.resolveContent(this, requestInfo.mediaTypes, requestInfo.examples) ?: mapOf(), content = feature.resolveContent(this, requestInfo.mediaTypes, requestInfo.examples as Map<String, Any>)
?: mapOf(),
required = requestInfo.required required = requestInfo.required
) )
} }
@ -109,13 +110,13 @@ object MethodParser {
* @param responseInfo response metadata * @param responseInfo response metadata
* @return Will return a generated [Pair] if responseInfo is not null * @return Will return a generated [Pair] if responseInfo is not null
*/ */
private fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?, feature: Kompendium): Pair<Int, Response<*>>? = fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?, feature: Kompendium): Pair<Int, Response>? =
when (responseInfo) { when (responseInfo) {
null -> null null -> null
else -> { else -> {
val specResponse = Response( val specResponse = Response(
description = responseInfo.description, description = responseInfo.description,
content = feature.resolveContent(this, responseInfo.mediaTypes, responseInfo.examples) content = feature.resolveContent(this, responseInfo.mediaTypes, responseInfo.examples as Map<String, Any>)
) )
Pair(responseInfo.status.value, specResponse) Pair(responseInfo.status.value, specResponse)
} }
@ -128,11 +129,11 @@ object MethodParser {
* @param examples Mapping of named examples of valid bodies. * @param examples Mapping of named examples of valid bodies.
* @return Named mapping of media types. * @return Named mapping of media types.
*/ */
private fun <F> Kompendium.resolveContent( fun Kompendium.resolveContent(
type: KType, type: KType,
mediaTypes: List<String>, mediaTypes: List<String>,
examples: Map<String, F> examples: Map<String, Any>
): Map<String, MediaType<F>>? { ): Map<String, MediaType>? {
val classifier = type.classifier as KClass<*> val classifier = type.classifier as KClass<*>
return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) { return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
mediaTypes.associateWith { mediaTypes.associateWith {
@ -161,25 +162,42 @@ object MethodParser {
* @return list of valid parameter specs as detailed by the [KType] members * @return list of valid parameter specs as detailed by the [KType] members
* @throws [IllegalStateException] if the class could not be parsed properly * @throws [IllegalStateException] if the class could not be parsed properly
*/ */
private fun KType.toParameterSpec(feature: Kompendium): List<Parameter> { fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> {
val clazz = classifier as KClass<*> val clazz = classifier as KClass<*>
return clazz.memberProperties.filter { prop -> return clazz.memberProperties
prop.findAnnotation<Param>() != null .filter { prop -> prop.hasAnnotation<Param>() }
}.map { prop -> .map { prop -> prop.toParameter(info, this, clazz, feature) }
val wrapperSchema = feature.config.cache[this.getSimpleSlug()]!! as ObjectSchema }
val anny = prop.findAnnotation<Param>()
?: error("Field ${prop.name} is not annotated with KompendiumParam") fun KProperty<*>.toParameter(
val schema = wrapperSchema.properties[prop.name] info: MethodInfo<*, *>,
?: error("Could not find component type for $prop") parentType: KType,
val defaultValue = getDefaultParameterValue(clazz, prop) parentClazz: KClass<*>,
Parameter( feature: Kompendium
name = prop.name, ): Parameter {
val wrapperSchema = feature.config.cache[parentType.getSimpleSlug()]!! as ObjectSchema
val anny = this.findAnnotation<Param>()
?: error("Field $name is not annotated with KompendiumParam")
val schema = wrapperSchema.properties[name]
?: error("Could not find component type for $this")
val defaultValue = getDefaultParameterValue(parentClazz, this)
return Parameter(
name = name,
`in` = anny.type.name.lowercase(Locale.getDefault()), `in` = anny.type.name.lowercase(Locale.getDefault()),
schema = schema.addDefault(defaultValue), schema = schema.addDefault(defaultValue),
description = schema.description, description = schema.description,
required = !prop.returnType.isMarkedNullable && defaultValue == null required = !returnType.isMarkedNullable && defaultValue == null,
examples = info.parameterExamples.mapToSpec(name)
) )
} }
fun Set<ParameterExample>.mapToSpec(parameterName: String): Map<String, Parameter.Example>? {
val filtered = filter { it.parameterName == parameterName }
return if (filtered.isEmpty()) {
null
} else {
filtered.associate { it.exampleName to Parameter.Example(it.exampleValue) }
}
} }
/** /**
@ -188,7 +206,7 @@ object MethodParser {
* @param prop the property in question * @param prop the property in question
* @return The default value if found * @return The default value if found
*/ */
private fun getDefaultParameterValue(clazz: KClass<*>, prop: KProperty<*>): Any? { fun getDefaultParameterValue(clazz: KClass<*>, prop: KProperty<*>): Any? {
val constructor = clazz.primaryConstructor val constructor = clazz.primaryConstructor
val parameterInQuestion = constructor val parameterInQuestion = constructor
?.parameters ?.parameters
@ -215,7 +233,7 @@ object MethodParser {
* @return value of the proper type to match param * @return value of the proper type to match param
* @throws [IllegalStateException] if parameter type is not one of the basic types supported below. * @throws [IllegalStateException] if parameter type is not one of the basic types supported below.
*/ */
private fun defaultValueInjector(param: KParameter): Any = when (param.type.classifier) { fun defaultValueInjector(param: KParameter): Any = when (param.type.classifier) {
String::class -> "test" String::class -> "test"
Boolean::class -> false Boolean::class -> false
Int::class -> 1 Int::class -> 1
@ -225,4 +243,10 @@ object MethodParser {
UUID::class -> UUID.randomUUID() UUID::class -> UUID.randomUUID()
else -> error("Unsupported Type") else -> error("Unsupported Type")
} }
/**
* Uses the built-in Ktor route path [Route.toString] but cuts out any meta route such as authentication... anything
* that matches the RegEx pattern `/\\(.+\\)`
*/
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
} }

View File

@ -8,6 +8,7 @@ import io.bkbn.kompendium.core.util.constrainedDoubleInfo
import io.bkbn.kompendium.core.util.constrainedIntInfo import io.bkbn.kompendium.core.util.constrainedIntInfo
import io.bkbn.kompendium.core.util.defaultField import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.defaultParameter import io.bkbn.kompendium.core.util.defaultParameter
import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.exclusiveMinMax import io.bkbn.kompendium.core.util.exclusiveMinMax
import io.bkbn.kompendium.core.util.formattedParam import io.bkbn.kompendium.core.util.formattedParam
import io.bkbn.kompendium.core.util.freeFormObject import io.bkbn.kompendium.core.util.freeFormObject
@ -27,6 +28,9 @@ import io.bkbn.kompendium.core.util.notarizedGetWithGenericErrorResponse
import io.bkbn.kompendium.core.util.notarizedGetWithMultipleThrowables import io.bkbn.kompendium.core.util.notarizedGetWithMultipleThrowables
import io.bkbn.kompendium.core.util.notarizedGetWithNotarizedException import io.bkbn.kompendium.core.util.notarizedGetWithNotarizedException
import io.bkbn.kompendium.core.util.notarizedGetWithPolymorphicErrorResponse import io.bkbn.kompendium.core.util.notarizedGetWithPolymorphicErrorResponse
import io.bkbn.kompendium.core.util.notarizedHeadModule
import io.bkbn.kompendium.core.util.notarizedOptionsModule
import io.bkbn.kompendium.core.util.notarizedPatchModule
import io.bkbn.kompendium.core.util.notarizedPostModule import io.bkbn.kompendium.core.util.notarizedPostModule
import io.bkbn.kompendium.core.util.notarizedPutModule import io.bkbn.kompendium.core.util.notarizedPutModule
import io.bkbn.kompendium.core.util.nullableField import io.bkbn.kompendium.core.util.nullableField
@ -55,56 +59,53 @@ import io.ktor.http.HttpStatusCode
class KompendiumTest : DescribeSpec({ class KompendiumTest : DescribeSpec({
describe("Notarized Open API Metadata Tests") { describe("Notarized Open API Metadata Tests") {
it("Can notarize a get request") { it("Can notarize a get request") {
// act
openApiTest("notarized_get.json") { notarizedGetModule() } openApiTest("notarized_get.json") { notarizedGetModule() }
} }
it("Can notarize a post request") { it("Can notarize a post request") {
// act
openApiTest("notarized_post.json") { notarizedPostModule() } openApiTest("notarized_post.json") { notarizedPostModule() }
} }
it("Can notarize a put request") { it("Can notarize a put request") {
// act
openApiTest("notarized_put.json") { notarizedPutModule() } openApiTest("notarized_put.json") { notarizedPutModule() }
} }
it("Can notarize a delete request") { it("Can notarize a delete request") {
// act
openApiTest("notarized_delete.json") { notarizedDeleteModule() } openApiTest("notarized_delete.json") { notarizedDeleteModule() }
} }
it("Can notarize a patch request") {
openApiTest("notarized_patch.json") { notarizedPatchModule() }
}
it("Can notarize a head request") {
openApiTest("notarized_head.json") { notarizedHeadModule() }
}
it("Can notarize an options request") {
openApiTest("notarized_options.json") { notarizedOptionsModule() }
}
it("Can notarize a complex type") { it("Can notarize a complex type") {
// act
openApiTest("complex_type.json") { complexType() } openApiTest("complex_type.json") { complexType() }
} }
it("Can notarize primitives") { it("Can notarize primitives") {
// act
openApiTest("notarized_primitives.json") { primitives() } openApiTest("notarized_primitives.json") { primitives() }
} }
it("Can notarize a top level list response") { it("Can notarize a top level list response") {
// act
openApiTest("response_list.json") { returnsList() } openApiTest("response_list.json") { returnsList() }
} }
it("Can notarize a route with non-required params") { it("Can notarize a route with non-required params") {
// act
openApiTest("non_required_params.json") { nonRequiredParamsGet() } openApiTest("non_required_params.json") { nonRequiredParamsGet() }
} }
} }
describe("Notarized Ktor Functionality Tests") { describe("Notarized Ktor Functionality Tests") {
it("Can notarized a get request and return the expected result") { it("Can notarized a get request and return the expected result") {
// act
apiFunctionalityTest("hey dude ‼️ congratz on the get request") { notarizedGetModule() } apiFunctionalityTest("hey dude ‼️ congratz on the get request") { notarizedGetModule() }
} }
it("Can notarize a post request and return the expected result") { it("Can notarize a post request and return the expected result") {
// act
apiFunctionalityTest( apiFunctionalityTest(
"hey dude ✌️ congratz on the post request", "hey dude ✌️ congratz on the post request",
httpMethod = HttpMethod.Post httpMethod = HttpMethod.Post
) { notarizedPostModule() } ) { notarizedPostModule() }
} }
it("Can notarize a put request and return the expected result") { it("Can notarize a put request and return the expected result") {
// act
apiFunctionalityTest("hey pal 🌝 whatcha doin' here?", httpMethod = HttpMethod.Put) { notarizedPutModule() } apiFunctionalityTest("hey pal 🌝 whatcha doin' here?", httpMethod = HttpMethod.Put) { notarizedPutModule() }
} }
it("Can notarize a delete request and return the expected result") { it("Can notarize a delete request and return the expected result") {
// act
apiFunctionalityTest( apiFunctionalityTest(
null, null,
httpMethod = HttpMethod.Delete, httpMethod = HttpMethod.Delete,
@ -112,59 +113,50 @@ class KompendiumTest : DescribeSpec({
) { notarizedDeleteModule() } ) { notarizedDeleteModule() }
} }
it("Can notarize the root route and return the expected result") { it("Can notarize the root route and return the expected result") {
// act
apiFunctionalityTest("☎️🏠🌲", "/") { rootModule() } apiFunctionalityTest("☎️🏠🌲", "/") { rootModule() }
} }
it("Can notarize a trailing slash route and return the expected result") { it("Can notarize a trailing slash route and return the expected result") {
// act
apiFunctionalityTest("🙀👾", "/test/") { trailingSlash() } apiFunctionalityTest("🙀👾", "/test/") { trailingSlash() }
} }
} }
describe("Route Parsing") { describe("Route Parsing") {
it("Can parse a simple path and store it under the expected route") { it("Can parse a simple path and store it under the expected route") {
// act
openApiTest("path_parser.json") { pathParsingTestModule() } openApiTest("path_parser.json") { pathParsingTestModule() }
} }
it("Can notarize the root route") { it("Can notarize the root route") {
// act
openApiTest("root_route.json") { rootModule() } openApiTest("root_route.json") { rootModule() }
} }
it("Can notarize a route under the root module without appending trailing slash") { it("Can notarize a route under the root module without appending trailing slash") {
// act
openApiTest("nested_under_root.json") { nestedUnderRootModule() } openApiTest("nested_under_root.json") { nestedUnderRootModule() }
} }
it("Can notarize a route with a trailing slash") { it("Can notarize a route with a trailing slash") {
// act
openApiTest("trailing_slash.json") { trailingSlash() } openApiTest("trailing_slash.json") { trailingSlash() }
} }
} }
describe("Exceptions") { describe("Exceptions") {
it("Can add an exception status code to a response") { it("Can add an exception status code to a response") {
// act
openApiTest("notarized_get_with_exception_response.json") { notarizedGetWithNotarizedException() } openApiTest("notarized_get_with_exception_response.json") { notarizedGetWithNotarizedException() }
} }
it("Can support multiple response codes") { it("Can support multiple response codes") {
// act
openApiTest("notarized_get_with_multiple_exception_responses.json") { notarizedGetWithMultipleThrowables() } openApiTest("notarized_get_with_multiple_exception_responses.json") { notarizedGetWithMultipleThrowables() }
} }
it("Can add a polymorphic exception response") { it("Can add a polymorphic exception response") {
// act
openApiTest("polymorphic_error_status_codes.json") { notarizedGetWithPolymorphicErrorResponse() } openApiTest("polymorphic_error_status_codes.json") { notarizedGetWithPolymorphicErrorResponse() }
} }
it("Can add a generic exception response") { it("Can add a generic exception response") {
// act
openApiTest("generic_exception.json") { notarizedGetWithGenericErrorResponse() } openApiTest("generic_exception.json") { notarizedGetWithGenericErrorResponse() }
} }
} }
describe("Examples") { describe("Examples") {
it("Can generate example response and request bodies") { it("Can generate example response and request bodies") {
// act
openApiTest("example_req_and_resp.json") { withExamples() } openApiTest("example_req_and_resp.json") { withExamples() }
} }
it("Can describe example parameters") {
openApiTest("example_parameters.json") { exampleParams() }
}
} }
describe("Defaults") { describe("Defaults") {
it("Can generate a default parameter values") { it("Can generate a default parameter values") {
// act
openApiTest("query_with_default_parameter.json") { withDefaultParameter() } openApiTest("query_with_default_parameter.json") { withDefaultParameter() }
} }
} }
@ -184,49 +176,38 @@ class KompendiumTest : DescribeSpec({
} }
describe("Polymorphism and Generics") { describe("Polymorphism and Generics") {
it("can generate a polymorphic response type") { it("can generate a polymorphic response type") {
// act
openApiTest("polymorphic_response.json") { polymorphicResponse() } openApiTest("polymorphic_response.json") { polymorphicResponse() }
} }
it("Can generate a collection with polymorphic response type") { it("Can generate a collection with polymorphic response type") {
// act
openApiTest("polymorphic_list_response.json") { polymorphicCollectionResponse() } openApiTest("polymorphic_list_response.json") { polymorphicCollectionResponse() }
} }
it("Can generate a map with a polymorphic response type") { it("Can generate a map with a polymorphic response type") {
// act
openApiTest("polymorphic_map_response.json") { polymorphicMapResponse() } openApiTest("polymorphic_map_response.json") { polymorphicMapResponse() }
} }
it("Can generate a polymorphic response from a sealed interface") { it("Can generate a polymorphic response from a sealed interface") {
// act
openApiTest("sealed_interface_response.json") { polymorphicInterfaceResponse() } openApiTest("sealed_interface_response.json") { polymorphicInterfaceResponse() }
} }
it("Can generate a response type with a generic type") { it("Can generate a response type with a generic type") {
// act
openApiTest("generic_response.json") { simpleGenericResponse() } openApiTest("generic_response.json") { simpleGenericResponse() }
} }
it("Can generate a polymorphic response type with generics") { it("Can generate a polymorphic response type with generics") {
// act
openApiTest("polymorphic_response_with_generics.json") { genericPolymorphicResponse() } openApiTest("polymorphic_response_with_generics.json") { genericPolymorphicResponse() }
} }
it("Can handle an absolutely psycho inheritance test") { it("Can handle an absolutely psycho inheritance test") {
// act
openApiTest("crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() } openApiTest("crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() }
} }
} }
describe("Miscellaneous") { describe("Miscellaneous") {
it("Can generate the necessary ReDoc home page") { it("Can generate the necessary ReDoc home page") {
// act
apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() } apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() }
} }
it("Can add an operation id to a notarized route") { it("Can add an operation id to a notarized route") {
// act
openApiTest("notarized_get_with_operation_id.json") { withOperationId() } openApiTest("notarized_get_with_operation_id.json") { withOperationId() }
} }
it("Can add an undeclared field") { it("Can add an undeclared field") {
// act
openApiTest("undeclared_field.json") { undeclaredType() } openApiTest("undeclared_field.json") { undeclaredType() }
} }
it("Can add a custom header parameter with a name override") { it("Can add a custom header parameter with a name override") {
// act
openApiTest("override_parameter_name.json") { headerParameter() } openApiTest("override_parameter_name.json") { headerParameter() }
} }
it("Can override field values via annotation") { it("Can override field values via annotation") {

View File

@ -2,6 +2,9 @@ package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.Notarized.notarizedDelete import io.bkbn.kompendium.core.Notarized.notarizedDelete
import io.bkbn.kompendium.core.Notarized.notarizedGet import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.Notarized.notarizedHead
import io.bkbn.kompendium.core.Notarized.notarizedOptions
import io.bkbn.kompendium.core.Notarized.notarizedPatch
import io.bkbn.kompendium.core.Notarized.notarizedPost import io.bkbn.kompendium.core.Notarized.notarizedPost
import io.bkbn.kompendium.core.Notarized.notarizedPut import io.bkbn.kompendium.core.Notarized.notarizedPut
import io.bkbn.kompendium.core.fixtures.Bibbity import io.bkbn.kompendium.core.fixtures.Bibbity
@ -105,6 +108,36 @@ fun Application.notarizedDeleteModule() {
} }
} }
fun Application.notarizedPatchModule() {
routing {
route("/test") {
notarizedPatch(TestResponseInfo.testPatchInfo) {
call.respondText { "hey dude ✌️ congratz on the patch request" }
}
}
}
}
fun Application.notarizedHeadModule() {
routing {
route("/test") {
notarizedHead(TestResponseInfo.testHeadInfo) {
call.response.status(HttpStatusCode.OK)
}
}
}
}
fun Application.notarizedOptionsModule() {
routing {
route("/test") {
notarizedOptions(TestResponseInfo.testOptionsInfo) {
call.response.status(HttpStatusCode.OK)
}
}
}
}
fun Application.notarizedPutModule() { fun Application.notarizedPutModule() {
routing { routing {
route("/test") { route("/test") {
@ -245,12 +278,12 @@ fun Application.withDefaultParameter() {
} }
} }
fun Application.withOperationId(){ fun Application.withOperationId() {
routing { routing {
route("/test") { route("/test") {
notarizedGet( notarizedGet(
info = TestResponseInfo.testGetInfo.copy(operationId = "getTest") info = TestResponseInfo.testGetInfo.copy(operationId = "getTest")
){ ) {
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.OK)
} }
} }
@ -531,3 +564,13 @@ fun Application.minMaxFreeForm() {
} }
} }
} }
fun Application.exampleParams() {
routing {
route("/test/{a}") {
notarizedGet(TestResponseInfo.exampleParams) {
call.respondText { "Hi 🌊" }
}
}
}
}

View File

@ -80,7 +80,7 @@
}, },
"required": [ "required": [
"org", "org",
"amazingField", "amazing_field",
"tables" "tables"
], ],
"type": "object" "type": "object"

View File

@ -0,0 +1,97 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "param stuff",
"description": "Cool stuff",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false,
"examples": {
"Testerino": {
"value": "a"
},
"Testerina": {
"value": "b"
}
}
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false,
"examples": {
"Wowza": {
"value": 6
}
}
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -62,7 +62,7 @@
} }
}, },
"required": [ "required": [
"fieldName", "field_name",
"b", "b",
"aaa" "aaa"
], ],

View File

@ -45,7 +45,7 @@
} }
}, },
"required": [ "required": [
"b" "real_name"
], ],
"type": "object" "type": "object"
} }

View File

@ -0,0 +1,49 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"head": {
"tags": [],
"summary": "Test head endpoint",
"description": "head test 💀",
"parameters": [],
"responses": {
"200": {
"description": "great!"
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,84 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"options": {
"tags": [],
"summary": "Test options",
"description": "endpoint of options",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "nice",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,64 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"patch": {
"tags": [],
"summary": "Test patch endpoint",
"description": "patch your tests here!",
"parameters": [],
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -82,7 +82,7 @@
} }
}, },
"required": [ "required": [
"fieldName", "field_name",
"b", "b",
"aaa" "aaa"
], ],

View File

@ -82,7 +82,7 @@
} }
}, },
"required": [ "required": [
"fieldName", "field_name",
"b", "b",
"aaa" "aaa"
], ],

View File

@ -1,12 +1,16 @@
package io.bkbn.kompendium.core.fixtures package io.bkbn.kompendium.core.fixtures
import io.bkbn.kompendium.core.metadata.ExceptionInfo import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.core.metadata.RequestInfo import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.DeleteInfo import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.HeadInfo
import io.bkbn.kompendium.core.metadata.method.OptionsInfo
import io.bkbn.kompendium.core.metadata.method.PatchInfo
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -15,6 +19,7 @@ object TestResponseInfo {
private val testGetListResponse = private val testGetListResponse =
ResponseInfo<List<TestResponse>>(HttpStatusCode.OK, "A Successful List-y Endeavor") ResponseInfo<List<TestResponse>>(HttpStatusCode.OK, "A Successful List-y Endeavor")
private val testPostResponse = ResponseInfo<TestCreatedResponse>(HttpStatusCode.Created, "A Successful Endeavor") private val testPostResponse = ResponseInfo<TestCreatedResponse>(HttpStatusCode.Created, "A Successful Endeavor")
private val testPatchResponse = ResponseInfo<TestResponse>(HttpStatusCode.Created, "A Successful Endeavor")
private val testPostResponseAgain = ResponseInfo<Boolean>(HttpStatusCode.Created, "A Successful Endeavor") private val testPostResponseAgain = ResponseInfo<Boolean>(HttpStatusCode.Created, "A Successful Endeavor")
private val testDeleteResponse = private val testDeleteResponse =
ResponseInfo<Unit>(HttpStatusCode.NoContent, "A Successful Endeavor", mediaTypes = emptyList()) ResponseInfo<Unit>(HttpStatusCode.NoContent, "A Successful Endeavor", mediaTypes = emptyList())
@ -75,6 +80,22 @@ object TestResponseInfo {
responseInfo = testPostResponse, responseInfo = testPostResponse,
requestInfo = complexRequest requestInfo = complexRequest
) )
val testPatchInfo = PatchInfo<Unit, TestRequest, TestResponse>(
summary = "Test patch endpoint",
description = "patch your tests here!",
responseInfo = testPatchResponse,
requestInfo = testRequest
)
val testHeadInfo = HeadInfo<Unit>(
summary = "Test head endpoint",
description = "head test 💀",
responseInfo = ResponseInfo(HttpStatusCode.OK, "great!")
)
val testOptionsInfo = OptionsInfo<TestParams, TestResponse>(
summary = "Test options",
description = "endpoint of options",
responseInfo = ResponseInfo(HttpStatusCode.OK, "nice")
)
val testPutInfoAlso = PutInfo<TestParams, TestRequest, TestCreatedResponse>( val testPutInfoAlso = PutInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test put endpoint", summary = "Test put endpoint",
description = "Put your tests here!", description = "Put your tests here!",
@ -246,5 +267,16 @@ object TestResponseInfo {
requestInfo = RequestInfo("cool") requestInfo = RequestInfo("cool")
) )
val exampleParams = GetInfo<TestParams, TestResponse>(
summary = "param stuff",
description = "Cool stuff",
responseInfo = simpleOkResponse(),
parameterExamples = setOf(
ParameterExample(TestParams::a.name, "Testerino", "a"),
ParameterExample(TestParams::a.name, "Testerina", "b"),
ParameterExample(TestParams::aa.name, "Wowza", 6),
)
)
private fun <T> simpleOkResponse() = ResponseInfo<T>(HttpStatusCode.OK, "A successful endeavor") private fun <T> simpleOkResponse() = ResponseInfo<T>(HttpStatusCode.OK, "A successful endeavor")
} }

View File

@ -1 +1,4 @@
# Module kompendium-locations # Module kompendium-locations
Adds support for Ktor [Locations](https://ktor.io/docs/locations.html) API. Any notarized location _must_ be provided
with a `TParam` annotated with `@Location`. Nested Locations are supported

View File

@ -1,5 +1,18 @@
plugins { plugins {
id("io.bkbn.sourdough.library") kotlin("jvm")
id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdough {
libraryName.set("Kompendium Locations")
libraryDescription.set("Supplemental library for Kompendium offering support for Ktor's Location API")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
} }
dependencies { dependencies {
@ -14,3 +27,11 @@ dependencies {
testImplementation(testFixtures(projects.kompendiumCore)) testImplementation(testFixtures(projects.kompendiumCore))
} }
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -0,0 +1,84 @@
package io.bkbn.kompendium.locations
import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.metadata.method.MethodInfo
import io.bkbn.kompendium.core.parser.IMethodParser
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.application.feature
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.routing.Route
import io.ktor.routing.application
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
@OptIn(KtorExperimentalLocationsAPI::class)
object LocationMethodParser : IMethodParser {
override fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> {
val clazzList = determineLocationParents(classifier!!)
return clazzList.associateWith { it.memberProperties }
.flatMap { (clazz, memberProperties) -> memberProperties.associateWith { clazz }.toList() }
.filter { (prop, _) -> prop.hasAnnotation<Param>() }
.map { (prop, clazz) -> prop.toParameter(info, clazz.createType(), clazz, feature) }
}
private fun determineLocationParents(classifier: KClassifier): List<KClass<*>> {
var clazz: KClass<*>? = classifier as KClass<*>
val clazzList = mutableListOf<KClass<*>>()
while (clazz != null) {
clazzList.add(clazz)
clazz = getLocationParent(clazz)
}
return clazzList
}
private fun getLocationParent(clazz: KClass<*>): KClass<*>? {
val parent = clazz.memberProperties
.find { (it.returnType.classifier as KAnnotatedElement).hasAnnotation<Location>() }
return parent?.returnType?.classifier as? KClass<*>
}
fun KClass<*>.calculateLocationPath(suffix: String = ""): String {
val locationAnnotation = this.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val parent = this.java.declaringClass?.kotlin
val newSuffix = locationAnnotation.path.plus(suffix)
return when (parent) {
null -> newSuffix
else -> parent.calculateLocationPath(newSuffix)
}
}
inline fun <reified TParam : Any> processBaseInfo(
paramType: KType,
requestType: KType,
responseType: KType,
info: MethodInfo<*, *>,
route: Route
): LocationBaseInfo {
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = route.calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
val feature = route.application.feature(Kompendium)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
return LocationBaseInfo(baseInfo, feature, pathWithLocation)
}
data class LocationBaseInfo(
val op: PathOperation,
val feature: Kompendium,
val path: String
)
}

View File

@ -1,28 +1,19 @@
package io.bkbn.kompendium.locations package io.bkbn.kompendium.locations
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
import io.bkbn.kompendium.core.MethodParser.parseMethodInfo
import io.bkbn.kompendium.core.Notarized.calculateRoutePath
import io.bkbn.kompendium.core.metadata.method.DeleteInfo import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation import io.bkbn.kompendium.oas.path.PathOperation
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.application.feature
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.handle import io.ktor.locations.handle
import io.ktor.locations.location import io.ktor.locations.location
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.application
import io.ktor.routing.method import io.ktor.routing.method
import io.ktor.util.pipeline.PipelineContext import io.ktor.util.pipeline.PipelineContext
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
/** /**
* This version of notarized routes leverages the Ktor [io.ktor.locations.Locations] plugin to provide type safe access * This version of notarized routes leverages the Ktor [io.ktor.locations.Locations] plugin to provide type safe access
@ -45,15 +36,8 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p }, postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType -> ): Route = methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>() val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" } lbi.feature.config.spec.paths[lbi.path]?.get = postProcess(lbi.op)
val feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.get = postProcess(baseInfo)
return location(TParam::class) { return location(TParam::class) {
method(HttpMethod.Get) { handle(body) } method(HttpMethod.Get) { handle(body) }
} }
@ -74,15 +58,8 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p }, postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType -> ): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>() val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" } lbi.feature.config.spec.paths[lbi.path]?.post = postProcess(lbi.op)
val feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.post = postProcess(baseInfo)
return location(TParam::class) { return location(TParam::class) {
method(HttpMethod.Post) { handle(body) } method(HttpMethod.Post) { handle(body) }
} }
@ -103,15 +80,8 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p }, postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType -> ): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>() val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" } lbi.feature.config.spec.paths[lbi.path]?.put = postProcess(lbi.op)
val feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.put = postProcess(baseInfo)
return location(TParam::class) { return location(TParam::class) {
method(HttpMethod.Put) { handle(body) } method(HttpMethod.Put) { handle(body) }
} }
@ -131,28 +101,10 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p }, postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType -> ): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>() val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" } lbi.feature.config.spec.paths[lbi.path]?.delete = postProcess(lbi.op)
val feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.delete = postProcess(baseInfo)
return location(TParam::class) { return location(TParam::class) {
method(HttpMethod.Delete) { handle(body) } method(HttpMethod.Delete) { handle(body) }
} }
} }
fun KClass<*>.calculateLocationPath(suffix: String = ""): String {
val locationAnnotation = this.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val parent = this.java.declaringClass?.kotlin
val newSuffix = locationAnnotation.path.plus(suffix)
return when (parent) {
null -> newSuffix
else -> parent.calculateLocationPath(newSuffix)
}
}
} }

View File

@ -40,6 +40,15 @@
}, },
"required": true, "required": true,
"deprecated": false "deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
} }
], ],
"responses": { "responses": {

View File

@ -40,6 +40,15 @@
}, },
"required": true, "required": true,
"deprecated": false "deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
} }
], ],
"responses": { "responses": {

View File

@ -40,6 +40,15 @@
}, },
"required": true, "required": true,
"deprecated": false "deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
} }
], ],
"requestBody": { "requestBody": {

View File

@ -40,6 +40,15 @@
}, },
"required": true, "required": true,
"deprecated": false "deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
} }
], ],
"requestBody": { "requestBody": {

View File

@ -1,3 +1,29 @@
plugins { plugins {
id("io.bkbn.sourdough.library") kotlin("jvm")
kotlin("plugin.serialization")
id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdough {
libraryName.set("Kompendium OpenAPI Spec")
libraryDescription.set("Collections of kotlin data classes modeling the OpenAPI specification")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
}
dependencies {
implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1")
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
} }

View File

@ -6,7 +6,9 @@ import io.bkbn.kompendium.oas.component.Components
import io.bkbn.kompendium.oas.info.Info import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.path.Path import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.server.Server import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.Serializable
@Serializable
data class OpenApiSpec( data class OpenApiSpec(
val openapi: String = "3.0.3", val openapi: String = "3.0.3",
val info: Info, val info: Info,

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.common package io.bkbn.kompendium.oas.common
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class ExternalDocumentation( data class ExternalDocumentation(
@Serializable(with = UriSerializer::class)
val url: URI, val url: URI,
val description: String? val description: String?
) )

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.common package io.bkbn.kompendium.oas.common
import kotlinx.serialization.Serializable
@Serializable
data class Tag( data class Tag(
val name: String, val name: String,
val description: String? = null, val description: String? = null,

View File

@ -1,7 +1,9 @@
package io.bkbn.kompendium.oas.component package io.bkbn.kompendium.oas.component
import io.bkbn.kompendium.oas.security.SecuritySchema import io.bkbn.kompendium.oas.security.SecuritySchema
import kotlinx.serialization.Serializable
@Serializable
data class Components( data class Components(
val securitySchemes: MutableMap<String, SecuritySchema> = mutableMapOf() val securitySchemes: MutableMap<String, SecuritySchema> = mutableMapOf()
) )

View File

@ -1,9 +1,13 @@
package io.bkbn.kompendium.oas.info package io.bkbn.kompendium.oas.info
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class Contact( data class Contact(
var name: String, var name: String,
@Serializable(with = UriSerializer::class)
var url: URI? = null, var url: URI? = null,
var email: String? = null // TODO Enforce email? var email: String? = null // TODO Enforce email?
) )

View File

@ -1,11 +1,15 @@
package io.bkbn.kompendium.oas.info package io.bkbn.kompendium.oas.info
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class Info( data class Info(
var title: String? = null, var title: String? = null,
var version: String? = null, var version: String? = null,
var description: String? = null, var description: String? = null,
@Serializable(with = UriSerializer::class)
var termsOfService: URI? = null, var termsOfService: URI? = null,
var contact: Contact? = null, var contact: Contact? = null,
var license: License? = null var license: License? = null

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.info package io.bkbn.kompendium.oas.info
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class License( data class License(
var name: String, var name: String,
@Serializable(with = UriSerializer::class)
var url: URI? = null var url: URI? = null
) )

View File

@ -2,7 +2,9 @@ package io.bkbn.kompendium.oas.path
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.bkbn.kompendium.oas.server.Server import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.Serializable
@Serializable
data class Path( data class Path(
var get: PathOperation? = null, var get: PathOperation? = null,
var put: PathOperation? = null, var put: PathOperation? = null,

View File

@ -6,7 +6,9 @@ import io.bkbn.kompendium.oas.payload.Payload
import io.bkbn.kompendium.oas.payload.Request import io.bkbn.kompendium.oas.payload.Request
import io.bkbn.kompendium.oas.payload.Response import io.bkbn.kompendium.oas.payload.Response
import io.bkbn.kompendium.oas.server.Server import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.Serializable
@Serializable
data class PathOperation( data class PathOperation(
var tags: Set<String> = emptySet(), var tags: Set<String> = emptySet(),
var summary: String? = null, var summary: String? = null,
@ -14,9 +16,9 @@ data class PathOperation(
var externalDocs: ExternalDocumentation? = null, var externalDocs: ExternalDocumentation? = null,
var operationId: String? = null, var operationId: String? = null,
var parameters: List<Parameter>? = null, var parameters: List<Parameter>? = null,
var requestBody: Request<*>? = null, var requestBody: Request? = null,
// TODO How to enforce `default` requirement 🧐 // TODO How to enforce `default` requirement 🧐
var responses: Map<Int, Response<*>>? = null, var responses: Map<Int, Response>? = null,
var callbacks: Map<String, Payload>? = null, // todo what is this? var callbacks: Map<String, Payload>? = null, // todo what is this?
var deprecated: Boolean = false, var deprecated: Boolean = false,
var security: List<Map<String, List<String>>>? = null, var security: List<Map<String, List<String>>>? = null,

View File

@ -1,10 +1,14 @@
package io.bkbn.kompendium.oas.payload package io.bkbn.kompendium.oas.payload
import io.bkbn.kompendium.oas.schema.ComponentSchema import io.bkbn.kompendium.oas.schema.ComponentSchema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
data class MediaType<T>( @Serializable
data class MediaType(
val schema: ComponentSchema, val schema: ComponentSchema,
val examples: Map<String, Example<T>>? = null val examples: Map<String, Example>? = null
) { ) {
data class Example<T>(val value: T) @Serializable
data class Example(val value: @Contextual Any)
} }

View File

@ -1,7 +1,10 @@
package io.bkbn.kompendium.oas.payload package io.bkbn.kompendium.oas.payload
import io.bkbn.kompendium.oas.schema.ComponentSchema import io.bkbn.kompendium.oas.schema.ComponentSchema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class Parameter( data class Parameter(
val name: String, val name: String,
val `in`: String, // TODO Enum? "query", "header", "path" or "cookie" val `in`: String, // TODO Enum? "query", "header", "path" or "cookie"
@ -11,5 +14,9 @@ data class Parameter(
val deprecated: Boolean = false, val deprecated: Boolean = false,
val allowEmptyValue: Boolean? = null, val allowEmptyValue: Boolean? = null,
val style: String? = null, val style: String? = null,
val explode: Boolean? = null val explode: Boolean? = null,
) val examples: Map<String, Example>? = null
) {
@Serializable
data class Example(val value: @Contextual Any)
}

View File

@ -1,7 +1,10 @@
package io.bkbn.kompendium.oas.payload package io.bkbn.kompendium.oas.payload
data class Request<T>( import kotlinx.serialization.Serializable
@Serializable
data class Request(
val description: String?, val description: String?,
val content: Map<String, MediaType<T>>, val content: Map<String, MediaType>,
val required: Boolean = false val required: Boolean = false
) : Payload ) : Payload

View File

@ -1,8 +1,11 @@
package io.bkbn.kompendium.oas.payload package io.bkbn.kompendium.oas.payload
data class Response<T>( import kotlinx.serialization.Serializable
@Serializable
data class Response(
val description: String? = null, val description: String? = null,
val headers: Map<String, Payload>? = null, val headers: Map<String, Payload>? = null,
val content: Map<String, MediaType<T>>? = null, val content: Map<String, MediaType>? = null,
val links: Map<String, Payload>? = null val links: Map<String, Payload>? = null
) : Payload ) : Payload

View File

@ -1,3 +1,6 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Serializable
@Serializable
data class AnyOfSchema(val anyOf: List<ComponentSchema>, override val description: String? = null) : ComponentSchema data class AnyOfSchema(val anyOf: List<ComponentSchema>, override val description: String? = null) : ComponentSchema

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class ArraySchema( data class ArraySchema(
val items: ComponentSchema, val items: ComponentSchema,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// constraints // constraints

View File

@ -1,5 +1,10 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.JsonClassDiscriminator
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("component_type") // todo figure out a way to filter this
sealed interface ComponentSchema { sealed interface ComponentSchema {
val description: String? val description: String?
get() = null get() = null

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class DictionarySchema( data class DictionarySchema(
val additionalProperties: ComponentSchema, val additionalProperties: ComponentSchema,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null override val nullable: Boolean? = null
) : TypedSchema { ) : TypedSchema {

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class EnumSchema( data class EnumSchema(
val `enum`: Set<String>, val `enum`: Set<String>,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null override val nullable: Boolean? = null
) : TypedSchema { ) : TypedSchema {

View File

@ -1,15 +1,23 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import io.bkbn.kompendium.oas.serialization.NumberSerializer
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class FormattedSchema( data class FormattedSchema(
val format: String, val format: String,
override val type: String, override val type: String,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// Constraints // Constraints
@Serializable(with = NumberSerializer::class)
val minimum: Number? = null, val minimum: Number? = null,
@Serializable(with = NumberSerializer::class)
val maximum: Number? = null, val maximum: Number? = null,
val exclusiveMinimum: Boolean? = null, val exclusiveMinimum: Boolean? = null,
val exclusiveMaximum: Boolean? = null, val exclusiveMaximum: Boolean? = null,
@Serializable(with = NumberSerializer::class)
val multipleOf: Number? = null, val multipleOf: Number? = null,
) : TypedSchema ) : TypedSchema

View File

@ -1,5 +1,9 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class FreeFormSchema( data class FreeFormSchema(
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// constraints // constraints
@ -8,5 +12,5 @@ data class FreeFormSchema(
) : TypedSchema { ) : TypedSchema {
val additionalProperties: Boolean = true val additionalProperties: Boolean = true
override val type: String = "object" override val type: String = "object"
override val default: Any? = null override val default: @Contextual Any? = null
} }

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class ObjectSchema( data class ObjectSchema(
val properties: Map<String, ComponentSchema>, val properties: Map<String, ComponentSchema>,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// constraints // constraints

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class SimpleSchema( data class SimpleSchema(
override val type: String, override val type: String,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// Constraints // Constraints

View File

@ -1,11 +1,13 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
import java.util.Locale import java.util.Locale
// TODO... is there even an official ktor api auth mechanism?? // TODO... is there even an official ktor api auth mechanism??
@Serializable
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
class ApiKeyAuth(val `in`: ApiKeyLocation, name: String) : SecuritySchema { class ApiKeyAuth(val `in`: ApiKeyLocation, val name: String) : SecuritySchema {
val type: String = "apiKey" val type: String = "apiKey"
enum class ApiKeyLocation { enum class ApiKeyLocation {

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
@Serializable
class BasicAuth : SecuritySchema { class BasicAuth : SecuritySchema {
val type: String = "http" val type: String = "http"
val scheme: String = "basic" val scheme: String = "basic"

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
@Serializable
data class BearerAuth(val bearerFormat: String? = null): SecuritySchema { data class BearerAuth(val bearerFormat: String? = null): SecuritySchema {
val type: String = "http" val type: String = "http"
val scheme: String = "bearer" val scheme: String = "bearer"

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
@Serializable
data class OAuth(val description: String? = null, val flows: Flows) : SecuritySchema { data class OAuth(val description: String? = null, val flows: Flows) : SecuritySchema {
val type: String = "oauth2" val type: String = "oauth2"
@Serializable
data class Flows( data class Flows(
val implicit: Implicit? = null, val implicit: Implicit? = null,
val authorizationCode: AuthorizationCode? = null, val authorizationCode: AuthorizationCode? = null,
@ -21,12 +25,14 @@ data class OAuth(val description: String? = null, val flows: Flows) : SecuritySc
get() = emptyMap() get() = emptyMap()
} }
@Serializable
data class Implicit( data class Implicit(
override val authorizationUrl: String, override val authorizationUrl: String,
override val refreshUrl: String? = null, override val refreshUrl: String? = null,
override val scopes: Map<String, String> = emptyMap() override val scopes: Map<String, String> = emptyMap()
) : Flow ) : Flow
@Serializable
data class AuthorizationCode( data class AuthorizationCode(
override val authorizationUrl: String, override val authorizationUrl: String,
override val tokenUrl: String? = null, override val tokenUrl: String? = null,
@ -34,12 +40,14 @@ data class OAuth(val description: String? = null, val flows: Flows) : SecuritySc
override val scopes: Map<String, String> = emptyMap() override val scopes: Map<String, String> = emptyMap()
) : Flow ) : Flow
@Serializable
data class Password( data class Password(
override val tokenUrl: String? = null, override val tokenUrl: String? = null,
override val refreshUrl: String? = null, override val refreshUrl: String? = null,
override val scopes: Map<String, String> = emptyMap() override val scopes: Map<String, String> = emptyMap()
) : Flow ) : Flow
@Serializable
data class ClientCredential( data class ClientCredential(
override val tokenUrl: String? = null, override val tokenUrl: String? = null,
override val refreshUrl: String? = null, override val refreshUrl: String? = null,

View File

@ -1,3 +1,8 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.JsonClassDiscriminator
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("schema_type") // todo figure out a way to filter this
sealed interface SecuritySchema sealed interface SecuritySchema

View File

@ -0,0 +1,27 @@
package io.bkbn.kompendium.oas.serialization
import kotlin.reflect.KClass
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.serializer
class AnySerializer<T : Any> : KSerializer<T> {
override fun serialize(encoder: Encoder, value: T) {
serialize(encoder, value, value::class as KClass<T>)
}
override fun deserialize(decoder: Decoder): T {
error("Abandon all hope ye who enter 💀")
}
override val descriptor: SerialDescriptor
get() = TODO("Not yet implemented")
@OptIn(InternalSerializationApi::class)
fun serialize(encoder: Encoder, obj: T, clazz: KClass<T>) {
clazz.serializer().serialize(encoder, obj)
}
}

View File

@ -0,0 +1,42 @@
package io.bkbn.kompendium.oas.serialization
import io.bkbn.kompendium.oas.schema.AnyOfSchema
import io.bkbn.kompendium.oas.schema.ArraySchema
import io.bkbn.kompendium.oas.schema.ComponentSchema
import io.bkbn.kompendium.oas.schema.DictionarySchema
import io.bkbn.kompendium.oas.schema.EnumSchema
import io.bkbn.kompendium.oas.schema.FormattedSchema
import io.bkbn.kompendium.oas.schema.FreeFormSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.bkbn.kompendium.oas.schema.SimpleSchema
import io.bkbn.kompendium.oas.security.ApiKeyAuth
import io.bkbn.kompendium.oas.security.BasicAuth
import io.bkbn.kompendium.oas.security.BearerAuth
import io.bkbn.kompendium.oas.security.OAuth
import io.bkbn.kompendium.oas.security.SecuritySchema
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
object KompendiumSerializersModule {
val module = SerializersModule {
polymorphic(ComponentSchema::class) {
subclass(SimpleSchema::class, SimpleSchema.serializer())
subclass(FormattedSchema::class, FormattedSchema.serializer())
subclass(ObjectSchema::class, ObjectSchema.serializer())
subclass(AnyOfSchema::class, AnyOfSchema.serializer())
subclass(ArraySchema::class, ArraySchema.serializer())
subclass(DictionarySchema::class, DictionarySchema.serializer())
subclass(EnumSchema::class, EnumSchema.serializer())
subclass(FreeFormSchema::class, FreeFormSchema.serializer())
}
polymorphic(SecuritySchema::class) {
subclass(ApiKeyAuth::class, ApiKeyAuth.serializer())
subclass(BasicAuth::class, BasicAuth.serializer())
subclass(BearerAuth::class, BearerAuth.serializer())
subclass(OAuth::class, OAuth.serializer())
}
contextual(Any::class, AnySerializer())
}
}

View File

@ -0,0 +1,23 @@
package io.bkbn.kompendium.oas.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
object NumberSerializer : KSerializer<Number> {
override fun deserialize(decoder: Decoder): Number = try {
decoder.decodeDouble()
} catch (_: SerializationException) {
decoder.decodeInt()
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Number", PrimitiveKind.DOUBLE)
override fun serialize(encoder: Encoder, value: Number) {
encoder.encodeString(value.toString())
}
}

View File

@ -0,0 +1,20 @@
package io.bkbn.kompendium.oas.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.net.URI
object UriSerializer : KSerializer<URI> {
override fun deserialize(decoder: Decoder): URI = URI.create(decoder.decodeString())
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("URI", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: URI) {
encoder.encodeString(value.toString())
}
}

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.server package io.bkbn.kompendium.oas.server
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class Server( data class Server(
@Serializable(with = UriSerializer::class)
val url: URI, val url: URI,
val description: String? = null, val description: String? = null,
var variables: Map<String, ServerVariable>? = null var variables: Map<String, ServerVariable>? = null

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.server package io.bkbn.kompendium.oas.server
import kotlinx.serialization.Serializable
@Serializable
data class ServerVariable( data class ServerVariable(
val `enum`: Set<String>, // todo enforce not empty val `enum`: Set<String>, // todo enforce not empty
val default: String, val default: String,

View File

@ -1,6 +1,12 @@
plugins { plugins {
kotlin("plugin.serialization") version "1.6.0" kotlin("jvm")
application kotlin("plugin.serialization")
id("io.bkbn.sourdough.application.jvm")
id("application")
}
sourdough {
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
} }
dependencies { dependencies {
@ -18,10 +24,23 @@ dependencies {
implementation(group = "io.ktor", name = "ktor-auth", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-auth", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-auth-jwt", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-auth-jwt", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-serialization", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-serialization", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-jackson", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-gson", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-locations", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-locations", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-webjars", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-webjars", version = ktorVersion)
// Logging
implementation("org.apache.logging.log4j:log4j-api-kotlin:1.1.0")
implementation("org.apache.logging.log4j:log4j-api:2.17.1")
implementation("org.apache.logging.log4j:log4j-core:2.17.1")
implementation("org.slf4j:slf4j-api:1.7.33")
implementation("org.slf4j:slf4j-simple:1.7.33")
implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1") implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1")
implementation(group = "joda-time", name = "joda-time", version = "2.10.13") implementation(group = "joda-time", name = "joda-time", version = "2.10.13")
} }
repositories {
mavenCentral()
}

View File

@ -7,12 +7,8 @@ import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.AuthPlaygroundToC.simpleAuthenticatedGet import io.bkbn.kompendium.playground.AuthPlaygroundToC.simpleAuthenticatedGet
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -27,7 +23,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
/** /**
* Application entrypoint. Run this and head on over to `localhost:8081/docs` * Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -44,10 +39,10 @@ fun main() {
// Application Module // Application Module
private fun Application.mainModule() { private fun Application.mainModule() {
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
install(Kompendium) { install(Kompendium) {
spec = AuthMetadata.spec spec = Util.baseSpec
} }
install(Authentication) { install(Authentication) {
// We can leverage the security config name to prevent typos // We can leverage the security config name to prevent typos
@ -73,36 +68,6 @@ private fun Application.mainModule() {
} }
} }
object AuthMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple API with documented Authentication",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
// This is where we define the available security configurations for our app // This is where we define the available security configurations for our app
object SecurityConfigurations { object SecurityConfigurations {
val basic = object : BasicAuthConfiguration { val basic = object : BasicAuthConfiguration {

View File

@ -1,5 +1,7 @@
package io.bkbn.kompendium.playground package io.bkbn.kompendium.playground
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.annotations.SerializedName
import io.bkbn.kompendium.annotations.Field import io.bkbn.kompendium.annotations.Field
import io.bkbn.kompendium.annotations.Param import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.annotations.ParamType import io.bkbn.kompendium.annotations.ParamType
@ -13,18 +15,15 @@ import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.BasicModels.BasicParameters import io.bkbn.kompendium.playground.BasicModels.BasicParameters
import io.bkbn.kompendium.playground.BasicModels.BasicResponse
import io.bkbn.kompendium.playground.BasicModels.BasicRequest import io.bkbn.kompendium.playground.BasicModels.BasicRequest
import io.bkbn.kompendium.playground.BasicModels.BasicResponse
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleDeleteRequest import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleDeleteRequest
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExample import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExample
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExampleWithParameters import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExampleWithParameters
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simplePostRequest import io.bkbn.kompendium.playground.BasicPlaygroundToC.simplePostRequest
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -37,8 +36,9 @@ import io.ktor.routing.routing
import io.ktor.serialization.json import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI import kotlinx.serialization.json.Json
import java.util.UUID import java.util.UUID
/** /**
@ -57,11 +57,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = BasicMetadata.spec spec = Util.baseSpec
} }
// Configures the routes for our API // Configures the routes for our API
routing { routing {
@ -161,38 +161,6 @@ object BasicPlaygroundToC {
) )
} }
// Contains the root metadata for our server. This is all the stuff that is defined once
// and cannot be inferred from the Ktor application
object BasicMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object BasicModels { object BasicModels {
@Serializable @Serializable
data class BasicResponse(val c: String) data class BasicResponse(val c: String)
@ -207,6 +175,9 @@ object BasicModels {
@Serializable @Serializable
data class BasicRequest( data class BasicRequest(
@JsonProperty("best_field")
@SerializedName("best_field")
@SerialName("best_field")
@Field(description = "This is a super important field!!", name = "best_field") @Field(description = "This is a super important field!!", name = "best_field")
val d: Boolean val d: Boolean
) )

View File

@ -23,16 +23,12 @@ import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedParams import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedParams
import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedRequest import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedRequest
import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedResponse import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedResponse
import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedGet import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedGet
import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedPost import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedPost
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -45,7 +41,6 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import java.net.URI
fun main() { fun main() {
embeddedServer( embeddedServer(
@ -59,11 +54,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = ConstrainedMetadata.spec spec = Util.baseSpec
} }
// Configures the routes for our API // Configures the routes for our API
routing { routing {
@ -104,36 +99,6 @@ object ConstrainedPlaygroundToC {
) )
} }
object ConstrainedMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object ConstrainedModels { object ConstrainedModels {
@Serializable @Serializable
data class ConstrainedResponse( data class ConstrainedResponse(

View File

@ -6,12 +6,8 @@ import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.ExceptionPlaygroundToC.simpleGetExample import io.bkbn.kompendium.playground.ExceptionPlaygroundToC.simpleGetExample
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -25,7 +21,6 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
import java.time.LocalDateTime import java.time.LocalDateTime
// Application Entrypoint // Application Entrypoint
@ -41,11 +36,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = ExceptionMetadata.spec spec = Util.baseSpec
} }
install(StatusPages) { install(StatusPages) {
exception<ExceptionModels.BadUserException> { exception<ExceptionModels.BadUserException> {
@ -70,36 +65,6 @@ private fun Application.mainModule() {
} }
} }
object ExceptionMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API with notarized exceptions",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
// This is a table of contents to hold all the metadata for our various API endpoints // This is a table of contents to hold all the metadata for our various API endpoints
object ExceptionPlaygroundToC { object ExceptionPlaygroundToC {
private val simpleException = ExceptionInfo<ExceptionModels.ExceptionResponse>( private val simpleException = ExceptionInfo<ExceptionModels.ExceptionResponse>(

View File

@ -5,12 +5,8 @@ import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.GenericPlaygroundToC.simpleGenericGet import io.bkbn.kompendium.playground.GenericPlaygroundToC.simpleGenericGet
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -22,7 +18,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
/** /**
* Application entrypoint. Run this and head on over to `localhost:8081/docs` * Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -40,11 +35,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = GenericMetadata.spec spec = Util.baseSpec
} }
routing { routing {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
@ -70,38 +65,6 @@ object GenericPlaygroundToC {
) )
} }
// Contains the root metadata for our server. This is all the stuff that is defined once
// and cannot be inferred from the Ktor application
object GenericMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API with Generic Data",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object GenericModels { object GenericModels {
@Serializable @Serializable
data class Foosy<T, K>(val test: T, val otherThing: List<K>) data class Foosy<T, K>(val test: T, val otherThing: List<K>)

View File

@ -0,0 +1,49 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedPost
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.gson.gson
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}
install(Kompendium) {
spec = Util.baseSpec
}
routing {
redoc()
route("/create") {
notarizedPost(BasicPlaygroundToC.simplePostRequest) {
val request = call.receive<BasicModels.BasicRequest>()
when (request.d) {
true -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "So it is true!"))
false -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "Oh, I knew it!"))
}
}
}
}
}

View File

@ -0,0 +1,52 @@
package io.bkbn.kompendium.playground
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedPost
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
install(Kompendium) {
spec = Util.baseSpec
}
routing {
redoc()
route("/create") {
notarizedPost(BasicPlaygroundToC.simplePostRequest) {
val request = call.receive<BasicModels.BasicRequest>()
when (request.d) {
true -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "So it is true!"))
false -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "Oh, I knew it!"))
}
}
}
}
}

View File

@ -7,14 +7,10 @@ import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.locations.NotarizedLocation.notarizedGet import io.bkbn.kompendium.locations.NotarizedLocation.notarizedGet
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.LocationsToC.ohBoiUCrazy import io.bkbn.kompendium.playground.LocationsToC.ohBoiUCrazy
import io.bkbn.kompendium.playground.LocationsToC.testLocation import io.bkbn.kompendium.playground.LocationsToC.testLocation
import io.bkbn.kompendium.playground.LocationsToC.testNestLocation import io.bkbn.kompendium.playground.LocationsToC.testNestLocation
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -28,7 +24,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
/** /**
* Application entrypoint. Run this and head on over to `localhost:8081/docs` * Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -44,10 +39,10 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
install(Kompendium) { install(Kompendium) {
spec = LocationMetadata.spec spec = Util.baseSpec
} }
install(Locations) install(Locations)
routing { routing {
@ -118,38 +113,6 @@ data class TestLocations(
} }
} }
// Contains the root metadata for our server. This is all the stuff that is defined once
// and cannot be inferred from the Ktor application
object LocationMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo Leveraging Ktor Locations",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object LocationModels { object LocationModels {
@Serializable @Serializable
data class ExampleResponse(val c: String) data class ExampleResponse(val c: String)

View File

@ -5,12 +5,8 @@ import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.PolymorphicPlaygroundToC.polymorphicExample import io.bkbn.kompendium.playground.PolymorphicPlaygroundToC.polymorphicExample
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -22,7 +18,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
/** /**
* Application entrypoint. Run this and head on over to `localhost:8081/docs` * Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -39,11 +34,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = PolymorphicMetadata.spec spec = Util.baseSpec
} }
// Configures the routes for our API // Configures the routes for our API
routing { routing {
@ -70,36 +65,6 @@ object PolymorphicPlaygroundToC {
) )
} }
object PolymorphicMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API with Polymorphic Models",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object PolymorphicModels { object PolymorphicModels {
sealed interface SlammaJamma sealed interface SlammaJamma

View File

@ -2,6 +2,7 @@ package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.Kompendium import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedGet import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.playground.util.Util
import io.bkbn.kompendium.swagger.swaggerUI import io.bkbn.kompendium.swagger.swaggerUI
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
@ -32,12 +33,12 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
install(Webjars) install(Webjars)
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = BasicMetadata.spec spec = Util.baseSpec
} }
// Configures the routes for our API // Configures the routes for our API
routing { routing {

View File

@ -0,0 +1,50 @@
package io.bkbn.kompendium.playground.util
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import java.net.URI
@OptIn(ExperimentalSerializationApi::class)
object Util {
val kotlinxConfig = Json {
classDiscriminator = "class"
serializersModule = KompendiumSerializersModule.module
prettyPrint = true
explicitNulls = false
encodeDefaults = true
}
val baseSpec = OpenApiSpec(
info = Info(
title = "Simple Demo API",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}

View File

@ -1 +1,3 @@
# Module kompendium-swagger-ui # Module kompendium-swagger-ui
Contains the code necessary to launch `swagger` as your documentation frontend.

View File

@ -1,10 +1,30 @@
plugins { plugins {
id("io.bkbn.sourdough.library") kotlin("jvm")
id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdough {
libraryName.set("Kompendium Swagger")
libraryDescription.set("Offers Swagger as a bundled WebJAR for Ktor")
} }
dependencies { dependencies {
val ktorVersion: String by project val ktorVersion: String by project
implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-webjars", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-webjars", version = ktorVersion)
implementation(group = "org.webjars", name = "swagger-ui", version = "4.1.3") implementation(group = "org.webjars", name = "swagger-ui", version = "4.2.1")
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
} }

15
renovate.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": [
"config:base"
],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true
},
{
"matchPackagePatterns": ["ktor"],
"groupName": "ktor"
}
]
}